Project import generated by Copybara.

GitOrigin-RevId: 06b0c815d958bef0a60978d0766d65af839e37b6
Change-Id: Ifac0c45ae509ca8a8ed793fac5c981d4e23d3bf9
diff --git a/.gitmodules b/.gitmodules
index caccb04..937bff2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,9 @@
 [submodule "third_party/abseil-cpp"]
 	path = third_party/abseil-cpp
 	url = https://github.com/abseil/abseil-cpp.git
+[submodule "third_party/benchmark"]
+	path = third_party/benchmark
+	url = https://github.com/google/benchmark.git
+[submodule "third_party/boringssl"]
+	path = third_party/boringssl
+	url = https://boringssl.googlesource.com/boringssl
diff --git a/Dockerfile b/Dockerfile
index 00dffc7..1f0462e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,7 +21,7 @@
 RUN apt upgrade -y
 
 # install cargo with default settings
-RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.68.1
+RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.72.0
 ENV PATH="/root/.cargo/bin:${PATH}"
 RUN cargo install --locked cargo-deny --color never 2>&1
 RUN cargo install cargo-fuzz --color never 2>&1
@@ -36,6 +36,7 @@
 RUN cargo install bindgen-cli --version 0.64.0
 RUN cargo install wasm-pack --color never 2>&1
 RUN rustup toolchain add nightly
+RUN rustup target add wasm32-unknown-unknown
 # boringssl build wants go
 RUN curl -L https://go.dev/dl/go1.20.2.linux-amd64.tar.gz | tar -C /usr/local -xz
 ENV PATH="$PATH:/usr/local/go/bin"
diff --git a/cmd-runner/Cargo.lock b/cmd-runner/Cargo.lock
new file mode 100644
index 0000000..4dadf2e
--- /dev/null
+++ b/cmd-runner/Cargo.lock
@@ -0,0 +1,30 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
+[[package]]
+name = "cmd-runner"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "owo-colors",
+ "shell-escape",
+]
+
+[[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
diff --git a/cmd-runner/Cargo.toml b/cmd-runner/Cargo.toml
new file mode 100644
index 0000000..e79317e
--- /dev/null
+++ b/cmd-runner/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "cmd-runner"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+anyhow = "1.0.64"
+shell-escape = "0.1.5"
+owo-colors = "3.5.0"
+
diff --git a/nearby/src/support.rs b/cmd-runner/src/lib.rs
similarity index 91%
rename from nearby/src/support.rs
rename to cmd-runner/src/lib.rs
index cc81352..fa2f1f4 100644
--- a/nearby/src/support.rs
+++ b/cmd-runner/src/lib.rs
@@ -33,7 +33,12 @@
     dir: &path::Path,
     cmd: impl AsRef<ffi::OsStr>,
 ) -> anyhow::Result<SuccessOutput> {
-    run::<C>(dir, process::Command::new("sh").current_dir(dir).args(["-c".as_ref(), cmd.as_ref()]))
+    run::<C>(
+        dir,
+        process::Command::new("sh")
+            .current_dir(dir)
+            .args(["-c".as_ref(), cmd.as_ref()]),
+    )
 }
 
 /// Run a cmd with explicit args directly without a shell.
@@ -53,7 +58,12 @@
     A: Clone + IntoIterator<Item = S>,
     S: AsRef<ffi::OsStr>,
 {
-    run::<C>(dir, process::Command::new(cmd.as_ref()).current_dir(dir).args(args))
+    run::<C>(
+        dir,
+        process::Command::new(cmd.as_ref())
+            .current_dir(dir)
+            .args(args),
+    )
 }
 
 /// Run the specified command.
@@ -73,8 +83,16 @@
         },
     );
 
-    let context = format!("{} [{}]", cmd_with_args.to_string_lossy(), dir.to_string_lossy(),);
-    println!("[{}] [{}]", cmd_with_args.to_string_lossy().green(), dir.to_string_lossy().blue());
+    let context = format!(
+        "{} [{}]",
+        cmd_with_args.to_string_lossy(),
+        dir.to_string_lossy(),
+    );
+    println!(
+        "[{}] [{}]",
+        cmd_with_args.to_string_lossy().green(),
+        dir.to_string_lossy().blue()
+    );
 
     let mut child = command
         .env_clear()
diff --git a/nearby/.cargo/config-boringssl.toml b/nearby/.cargo/config-boringssl.toml
index bab3518..2bde6df 100644
--- a/nearby/.cargo/config-boringssl.toml
+++ b/nearby/.cargo/config-boringssl.toml
@@ -1,13 +1,9 @@
 # The packages to override
 paths = [
-    "../boringssl-build/boringssl/rust/bssl-crypto",
-    "../boringssl-build/boringssl/rust/bssl-sys",
+    "../third_party/boringssl/rust/bssl-sys",
     "../boringssl-build/rust-openssl/openssl",
     "../boringssl-build/rust-openssl/openssl-sys",
 ]
 
 [env]
 WORKSPACE_DIR = { value = "", relative = true }
-
-
-
diff --git a/nearby/Cargo.lock b/nearby/Cargo.lock
index 76582bf..7441c1b 100644
--- a/nearby/Cargo.lock
+++ b/nearby/Cargo.lock
@@ -3,15 +3,6 @@
 version = 3
 
 [[package]]
-name = "addr2line"
-version = "0.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
-dependencies = [
- "gimli",
-]
-
-[[package]]
 name = "adler"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -40,6 +31,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -55,17 +60,6 @@
 ]
 
 [[package]]
-name = "ahash"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
-]
-
-[[package]]
 name = "aho-corasick"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -81,12 +75,6 @@
 checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
 
 [[package]]
-name = "allocator-api2"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
-
-[[package]]
 name = "android-tzdata"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -188,21 +176,6 @@
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
-name = "backtrace"
-version = "0.3.68"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
-dependencies = [
- "addr2line",
- "cc",
- "cfg-if",
- "libc",
- "miniz_oxide",
- "object",
- "rustc-demangle",
-]
-
-[[package]]
 name = "base16ct"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -268,6 +241,13 @@
 [[package]]
 name = "bssl-crypto"
 version = "0.1.0"
+dependencies = [
+ "bssl-sys 0.1.0",
+]
+
+[[package]]
+name = "bssl-sys"
+version = "0.1.0"
 
 [[package]]
 name = "bssl-sys"
@@ -290,20 +270,20 @@
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "base64 0.21.2",
  "chrono",
  "clap 4.3.19",
+ "cmd-runner",
  "crossbeam",
  "env_logger",
+ "file-header",
  "globset",
  "log",
  "owo-colors",
- "reqwest",
  "semver",
+ "serde_json",
  "shell-escape",
  "tempfile",
  "thiserror",
- "tinytemplate",
  "walkdir",
  "which",
 ]
@@ -471,6 +451,15 @@
 checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
 
 [[package]]
+name = "cmd-runner"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "owo-colors",
+ "shell-escape",
+]
+
+[[package]]
 name = "colorchoice"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -487,6 +476,10 @@
 ]
 
 [[package]]
+name = "connections_adv"
+version = "0.1.0"
+
+[[package]]
 name = "const-oid"
 version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -652,6 +645,7 @@
  "hex-literal",
  "rand",
  "rand_ext",
+ "tinyvec",
 ]
 
 [[package]]
@@ -660,7 +654,6 @@
 dependencies = [
  "bssl-crypto",
  "crypto_provider",
- "crypto_provider_stubs",
 ]
 
 [[package]]
@@ -685,7 +678,7 @@
  "hex-literal",
  "openssl",
  "ouroboros",
- "rstest 0.17.0",
+ "rstest",
 ]
 
 [[package]]
@@ -694,6 +687,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -731,7 +725,7 @@
  "hex-literal",
  "rand",
  "rand_ext",
- "rstest 0.17.0",
+ "rstest",
  "rstest_reuse",
  "test_helper",
  "wycheproof",
@@ -748,9 +742,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -824,9 +818,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -862,15 +856,6 @@
 ]
 
 [[package]]
-name = "encoding_rs"
-version = "0.8.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
 name = "env_logger"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -922,9 +907,22 @@
 
 [[package]]
 name = "fiat-crypto"
-version = "0.1.20"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
+checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
+
+[[package]]
+name = "file-header"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5568149106e77ae33bc3a2c3ef3839cbe63ffa4a8dd4a81612a6f9dfdbc2e9f"
+dependencies = [
+ "crossbeam",
+ "lazy_static",
+ "license",
+ "thiserror",
+ "walkdir",
+]
 
 [[package]]
 name = "flate2"
@@ -958,110 +956,6 @@
 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
-name = "form_urlencoded"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
-dependencies = [
- "percent-encoding",
-]
-
-[[package]]
-name = "futures"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-channel"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
-dependencies = [
- "futures-core",
- "futures-sink",
-]
-
-[[package]]
-name = "futures-core"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
-name = "futures-io"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
-
-[[package]]
-name = "futures-macro"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.28",
-]
-
-[[package]]
-name = "futures-sink"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
-
-[[package]]
-name = "futures-task"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
-
-[[package]]
-name = "futures-timer"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c"
-
-[[package]]
-name = "futures-util"
-version = "0.3.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-macro",
- "futures-sink",
- "futures-task",
- "memchr",
- "pin-project-lite",
- "pin-utils",
- "slab",
-]
-
-[[package]]
 name = "generic-array"
 version = "0.14.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1084,10 +978,14 @@
 ]
 
 [[package]]
-name = "gimli"
-version = "0.27.3"
+name = "ghash"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
 
 [[package]]
 name = "globset"
@@ -1114,25 +1012,6 @@
 ]
 
 [[package]]
-name = "h2"
-version = "0.3.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
-dependencies = [
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "futures-util",
- "http",
- "indexmap",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
-[[package]]
 name = "half"
 version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1144,10 +1023,8 @@
 dependencies = [
  "criterion",
  "crypto_provider",
- "hashbrown 0.14.0",
- "lock_api",
- "portable-atomic",
- "spin 0.9.8",
+ "lazy_static",
+ "lock_adapter",
 ]
 
 [[package]]
@@ -1157,16 +1034,6 @@
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
-name = "hashbrown"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
-dependencies = [
- "ahash",
- "allocator-api2",
-]
-
-[[package]]
 name = "hdrhistogram"
 version = "7.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1232,84 +1099,12 @@
 ]
 
 [[package]]
-name = "http"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
-[[package]]
-name = "http-body"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
-dependencies = [
- "bytes",
- "http",
- "pin-project-lite",
-]
-
-[[package]]
-name = "httparse"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
-
-[[package]]
-name = "httpdate"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
-
-[[package]]
 name = "humantime"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
-name = "hyper"
-version = "0.14.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-core",
- "futures-util",
- "h2",
- "http",
- "http-body",
- "httparse",
- "httpdate",
- "itoa",
- "pin-project-lite",
- "socket2",
- "tokio",
- "tower-service",
- "tracing",
- "want",
-]
-
-[[package]]
-name = "hyper-rustls"
-version = "0.24.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97"
-dependencies = [
- "futures-util",
- "http",
- "hyper",
- "rustls",
- "tokio",
- "tokio-rustls",
-]
-
-[[package]]
 name = "iana-time-zone"
 version = "0.1.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1333,23 +1128,13 @@
 ]
 
 [[package]]
-name = "idna"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
-dependencies = [
- "unicode-bidi",
- "unicode-normalization",
-]
-
-[[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",
+ "hashbrown",
 ]
 
 [[package]]
@@ -1369,12 +1154,6 @@
 ]
 
 [[package]]
-name = "ipnet"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
-
-[[package]]
 name = "is-terminal"
 version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1518,6 +1297,17 @@
 checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
 
 [[package]]
+name = "license"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "778718185117620a06e95d2b1e57d50166b1d6bfad93c8abfc1b3344c863ad8c"
+dependencies = [
+ "reword",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
 name = "linux-raw-sys"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1562,12 +1352,6 @@
 ]
 
 [[package]]
-name = "mime"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
-
-[[package]]
 name = "minimal-lexical"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1583,17 +1367,6 @@
 ]
 
 [[package]]
-name = "mio"
-version = "0.8.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
-dependencies = [
- "libc",
- "wasi",
- "windows-sys 0.48.0",
-]
-
-[[package]]
 name = "nom"
 version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1622,6 +1395,8 @@
  "np_hkdf",
  "rand",
  "rand_ext",
+ "rmp-serde",
+ "serde",
  "serde_json",
  "sink",
  "strum",
@@ -1649,8 +1424,9 @@
  "crypto_provider",
  "crypto_provider_default",
  "handle_map",
+ "lazy_static",
+ "lock_adapter",
  "np_adv",
- "spin 0.9.8",
 ]
 
 [[package]]
@@ -1711,15 +1487,6 @@
 ]
 
 [[package]]
-name = "object"
-version = "0.31.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1"
-dependencies = [
- "memchr",
-]
-
-[[package]]
 name = "once_cell"
 version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1769,7 +1536,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
 dependencies = [
- "bssl-sys",
+ "bssl-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cc",
  "libc",
  "pkg-config",
@@ -1823,22 +1590,10 @@
 ]
 
 [[package]]
-name = "percent-encoding"
-version = "2.3.0"
+name = "paste"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
-
-[[package]]
-name = "pin-project-lite"
-version = "0.2.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57"
-
-[[package]]
-name = "pin-utils"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 
 [[package]]
 name = "pkcs8"
@@ -1903,12 +1658,6 @@
 ]
 
 [[package]]
-name = "portable-atomic"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f32154ba0af3a075eefa1eda8bb414ee928f62303a54ea85b8d6638ff1a6ee9e"
-
-[[package]]
 name = "ppv-lite86"
 version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2140,69 +1889,34 @@
 checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
 
 [[package]]
-name = "reqwest"
-version = "0.11.18"
+name = "reword"
+version = "7.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
+checksum = "fe272098dce9ed76b479995953f748d1851261390b08f8a0ff619c885a1f0765"
 dependencies = [
- "base64 0.21.2",
- "bytes",
- "encoding_rs",
- "futures-core",
- "futures-util",
- "h2",
- "http",
- "http-body",
- "hyper",
- "hyper-rustls",
- "ipnet",
- "js-sys",
- "log",
- "mime",
- "once_cell",
- "percent-encoding",
- "pin-project-lite",
- "rustls",
- "rustls-pemfile",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "rmp"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
+dependencies = [
+ "byteorder",
+ "num-traits",
+ "paste",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
+dependencies = [
+ "byteorder",
+ "rmp",
  "serde",
- "serde_json",
- "serde_urlencoded",
- "tokio",
- "tokio-rustls",
- "tower-service",
- "url",
- "wasm-bindgen",
- "wasm-bindgen-futures",
- "web-sys",
- "webpki-roots",
- "winreg",
-]
-
-[[package]]
-name = "ring"
-version = "0.16.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
-dependencies = [
- "cc",
- "libc",
- "once_cell",
- "spin 0.5.2",
- "untrusted",
- "web-sys",
- "winapi",
-]
-
-[[package]]
-name = "rstest"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b07f2d176c472198ec1e6551dc7da28f1c089652f66a7b722676c2238ebc0edf"
-dependencies = [
- "futures",
- "futures-timer",
- "rstest_macros 0.16.0",
- "rustc_version",
 ]
 
 [[package]]
@@ -2211,26 +1925,12 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "de1bb486a691878cd320c2f0d319ba91eeaa2e894066d8b5f8f117c000e9d962"
 dependencies = [
- "rstest_macros 0.17.0",
+ "rstest_macros",
  "rustc_version",
 ]
 
 [[package]]
 name = "rstest_macros"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7229b505ae0706e64f37ffc54a9c163e11022a6636d58fe1f3f52018257ff9f7"
-dependencies = [
- "cfg-if",
- "proc-macro2",
- "quote",
- "rustc_version",
- "syn 1.0.109",
- "unicode-ident",
-]
-
-[[package]]
-name = "rstest_macros"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "290ca1a1c8ca7edb7c3283bd44dc35dd54fdec6253a3912e201ba1072018fca8"
@@ -2245,23 +1945,17 @@
 
 [[package]]
 name = "rstest_reuse"
-version = "0.5.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45f80dcc84beab3a327bbe161f77db25f336a1452428176787c8c79ac79d7073"
+checksum = "88530b681abe67924d42cca181d070e3ac20e0740569441a9e35a7cedd2b34a4"
 dependencies = [
  "quote",
  "rand",
  "rustc_version",
- "syn 1.0.109",
+ "syn 2.0.28",
 ]
 
 [[package]]
-name = "rustc-demangle"
-version = "0.1.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
-
-[[package]]
 name = "rustc_version"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2284,37 +1978,6 @@
 ]
 
 [[package]]
-name = "rustls"
-version = "0.21.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
-dependencies = [
- "log",
- "ring",
- "rustls-webpki",
- "sct",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "1.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
-dependencies = [
- "base64 0.21.2",
-]
-
-[[package]]
-name = "rustls-webpki"
-version = "0.101.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
 name = "rustversion"
 version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2342,16 +2005,6 @@
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
-name = "sct"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
 name = "sec1"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2372,18 +2025,18 @@
 
 [[package]]
 name = "serde"
-version = "1.0.179"
+version = "1.0.192"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a5bf42b8d227d4abf38a1ddb08602e229108a517cd4e5bb28f9c7eaafdce5c0"
+checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.179"
+version = "1.0.192"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "741e124f5485c7e60c03b043f79f320bff3527f4bbf12cf3831750dc46a0ec2c"
+checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2402,18 +2055,6 @@
 ]
 
 [[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
-dependencies = [
- "form_urlencoded",
- "itoa",
- "ryu",
- "serde",
-]
-
-[[package]]
 name = "sha2"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2444,25 +2085,6 @@
 ]
 
 [[package]]
-name = "slab"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
-dependencies = [
- "autocfg",
-]
-
-[[package]]
-name = "socket2"
-version = "0.4.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
-dependencies = [
- "libc",
- "winapi",
-]
-
-[[package]]
 name = "spin"
 version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2501,21 +2123,21 @@
 
 [[package]]
 name = "strum"
-version = "0.24.1"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
 
 [[package]]
 name = "strum_macros"
-version = "0.24.3"
+version = "0.25.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
 dependencies = [
  "heck",
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 1.0.109",
+ "syn 2.0.28",
 ]
 
 [[package]]
@@ -2617,88 +2239,6 @@
 version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "tokio"
-version = "1.29.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
-dependencies = [
- "autocfg",
- "backtrace",
- "bytes",
- "libc",
- "mio",
- "num_cpus",
- "pin-project-lite",
- "socket2",
- "windows-sys 0.48.0",
-]
-
-[[package]]
-name = "tokio-rustls"
-version = "0.24.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
-dependencies = [
- "rustls",
- "tokio",
-]
-
-[[package]]
-name = "tokio-util"
-version = "0.7.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "pin-project-lite",
- "tokio",
- "tracing",
-]
-
-[[package]]
-name = "tower-service"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
-
-[[package]]
-name = "tracing"
-version = "0.1.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
-dependencies = [
- "cfg-if",
- "pin-project-lite",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-core"
-version = "0.1.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
-dependencies = [
- "once_cell",
-]
-
-[[package]]
-name = "try-lock"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
 [[package]]
 name = "typenum"
@@ -2711,8 +2251,7 @@
 version = "0.1.0"
 dependencies = [
  "cfg-if",
- "crypto_provider_openssl",
- "crypto_provider_rustcrypto",
+ "crypto_provider_default",
  "lazy_static",
  "lock_adapter",
  "log",
@@ -2729,11 +2268,10 @@
  "bytes",
  "criterion",
  "crypto_provider",
- "crypto_provider_openssl",
+ "crypto_provider_default",
  "crypto_provider_rustcrypto",
  "nom",
  "rand",
- "rstest 0.16.0",
  "ukey2_proto",
  "ukey2_rs",
 ]
@@ -2743,8 +2281,7 @@
 version = "0.1.0"
 dependencies = [
  "cfg-if",
- "crypto_provider_openssl",
- "crypto_provider_rustcrypto",
+ "crypto_provider_default",
  "jni",
  "lazy_static",
  "lock_adapter",
@@ -2769,13 +2306,11 @@
 version = "0.1.0"
 dependencies = [
  "crypto_provider",
- "crypto_provider_openssl",
- "crypto_provider_rustcrypto",
+ "crypto_provider_default",
  "derive-getters",
  "log",
  "num-bigint",
  "rand",
- "rstest 0.16.0",
  "sha2",
  "ukey2_proto",
 ]
@@ -2791,25 +2326,16 @@
 ]
 
 [[package]]
-name = "unicode-bidi"
-version = "0.3.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
-
-[[package]]
 name = "unicode-ident"
 version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
 
 [[package]]
-name = "unicode-normalization"
-version = "0.1.22"
+name = "unicode-segmentation"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
-dependencies = [
- "tinyvec",
-]
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
 
 [[package]]
 name = "universal-hash"
@@ -2822,23 +2348,6 @@
 ]
 
 [[package]]
-name = "untrusted"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
-
-[[package]]
-name = "url"
-version = "2.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
-dependencies = [
- "form_urlencoded",
- "idna",
- "percent-encoding",
-]
-
-[[package]]
 name = "utf8parse"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2867,15 +2376,6 @@
 ]
 
 [[package]]
-name = "want"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
-dependencies = [
- "try-lock",
-]
-
-[[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2907,18 +2407,6 @@
 ]
 
 [[package]]
-name = "wasm-bindgen-futures"
-version = "0.4.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
-dependencies = [
- "cfg-if",
- "js-sys",
- "wasm-bindgen",
- "web-sys",
-]
-
-[[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2958,25 +2446,6 @@
 ]
 
 [[package]]
-name = "webpki"
-version = "0.22.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
-dependencies = [
- "ring",
- "untrusted",
-]
-
-[[package]]
-name = "webpki-roots"
-version = "0.22.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
-dependencies = [
- "webpki",
-]
-
-[[package]]
 name = "which"
 version = "4.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3160,21 +2629,12 @@
 checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
 
 [[package]]
-name = "winreg"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
-dependencies = [
- "winapi",
-]
-
-[[package]]
 name = "wycheproof"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "183c789620c674b79dac33cd3aadb6c8006b66cba6a680402235aaebc743e3df"
+checksum = "e639f57253b80c6584b378011aec0fed61c4c21d7a4b97c4d9d7eaf35ca77d12"
 dependencies = [
- "base64 0.13.1",
+ "base64 0.21.2",
  "hex",
  "serde",
  "serde_json",
@@ -3182,9 +2642,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core 0.6.4",
@@ -3217,6 +2677,7 @@
  "rand_pcg",
  "regex",
  "test_helper",
+ "wycheproof",
  "xts-mode",
 ]
 
diff --git a/nearby/Cargo.toml b/nearby/Cargo.toml
index 361628a..1416e24 100644
--- a/nearby/Cargo.toml
+++ b/nearby/Cargo.toml
@@ -1,13 +1,12 @@
 [workspace]
 members = [
-    "connections/ukey2/lock_adapter",
+    "connections/connections_adv/connections_adv",
     "connections/ukey2/ukey2",
     "connections/ukey2/ukey2_connections",
     "connections/ukey2/ukey2_c_ffi",
     "connections/ukey2/ukey2_jni",
     "connections/ukey2/ukey2_proto",
     "connections/ukey2/ukey2_shell",
-    "crypto/bssl-crypto",
     "crypto/crypto_provider",
     "crypto/crypto_provider_openssl",
     "crypto/crypto_provider_rustcrypto",
@@ -16,7 +15,6 @@
     "crypto/crypto_provider_default",
     "crypto/rand_core_05_adapter",
     "presence/array_view",
-    "presence/handle_map",
     "presence/ldt",
     "presence/ldt_np_adv",
     "presence/ldt_np_jni",
@@ -29,6 +27,8 @@
     "presence/sink",
     "presence/test_helper",
     "presence/xts_aes",
+    "util/lock_adapter",
+    "util/handle_map",
 ]
 
 # TODO: remove ldt_np_adv_ffi once support for no_std + alloc no longer requires nightly
@@ -44,14 +44,15 @@
 # local crates
 array_ref = { path = "presence/array_ref" }
 array_view = { path = "presence/array_view" }
-crypto_provider = { path = "crypto/crypto_provider" }
+crypto_provider = { path = "crypto/crypto_provider", default-features = false }
 crypto_provider_default = { path = "crypto/crypto_provider_default", default-features = false }
 crypto_provider_openssl = { path = "crypto/crypto_provider_openssl" }
+crypto_provider_boringssl = { path = "crypto/crypto_provider_boringssl" }
 crypto_provider_rustcrypto = { path = "crypto/crypto_provider_rustcrypto" }
 crypto_provider_stubs = { path = "crypto/crypto_provider_stubs" }
 crypto_provider_test = { path = "crypto/crypto_provider_test" }
-lock_adapter = { path = "connections/ukey2/lock_adapter" }
-handle_map = { path = "presence/handle_map" }
+lock_adapter = { path = "util/lock_adapter" }
+handle_map = { path = "util/handle_map" }
 rand_core_05_adapter = { path = "crypto/rand_core_05_adapter" }
 rand_ext = { path = "presence/rand_ext" }
 test_helper = { path = "presence/test_helper" }
@@ -68,33 +69,33 @@
 
 # from crates.io
 rand = { version = "0.8.5", default-features = false }
-rand_core = "0.6.4"
+rand_core = { version = "0.6.4", features = ["getrandom"] }
 rand_pcg = "0.3.1"
 sha2 = { version = "0.10.6", default-features = false }
 aes = "0.8.2"
-cbc = { version = "0.1.2", features = ["alloc", "block-padding"] }
+cbc = { version = "0.1.2", features = ["block-padding"] }
 ctr = "0.9.1"
-hashbrown = "0.14.0"
 hkdf = "0.12.3"
 hmac = "0.12.1"
-ed25519-dalek = { version = "2.0.0-rc.3", default-features = false }
+ed25519-dalek = { version = "2.0.0", default-features = false }
 ed25519 = "2.2.0"
-aes-gcm = "0.10.1"
+aes-gcm = "0.10.3"
 hex = "0.4.3"
+serde = { version = "1.0.189" }
 serde_json = { version = "1.0.96", features = [
     "alloc",
 ], default-features = false }
 base64 = "0.21.0"
-x25519-dalek = { version = "2.0.0-rc.3", default-features = false }
+x25519-dalek = { version = "2.0.0", default-features = false }
 subtle = { version = "2.5.0", default-features = false }
 rand_chacha = { version = "0.3.1", default-features = false }
 p256 = { version = "0.13.2", default-features = false }
 sec1 = "0.7.2"
-portable-atomic = "1.3.2"
 protobuf = "3.2.0"
 protobuf-codegen = "3.2.0"
+reqwest = { version = "0.11.19", default-features = false, features = ["blocking", "rustls-tls"] }
 jni = "0.21.1"
-lock_api = "0.4.9"
+lock_api = "0.4.11"
 spin = { version = "0.9.8", features = ["once", "lock_api", "rwlock"] }
 anyhow = "1.0.64"
 log = "0.4.17"
@@ -108,18 +109,19 @@
 blake2 = "0.10.4"
 hdrhistogram = "7.5.0"
 regex = "1.7.0"
-tokio = { version = "1.20.3", features = ["full"] }
+tokio = { version = "1.32.0", features = ["full"] }
 xts-mode = "0.5.1"
 rstest = { version = "0.17.0", default-features = false }
-rstest_reuse = "0.5.0"
-wycheproof = "0.4.0"
-chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
+rstest_reuse = "0.6.0"
+wycheproof = "0.5.1"
+chrono = { version = "0.4.26", default-features = false, features = ["clock"] }
 tempfile = "3.5.0"
 thiserror = "1.0.40"
-tinyvec = {version = "1.6.0", features = ["rustc_1_55"]}
+tinyvec = { version = "1.6.0", features = ["rustc_1_55"] }
 mlua = "0.8.8"
-strum = "0.24.1"
-strum_macros = "0.24.2"
+strum = { version = "0.25.0", default-features=false }
+strum_macros = { version = "0.25.3", default-features=false }
+owo-colors = "3.5.0"
 
 [workspace.package]
 version = "0.1.0"
@@ -150,27 +152,25 @@
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
+rust-version = "1.71.0"
 
 [dependencies]
 clap.workspace = true
+cmd-runner = { path = "../cmd-runner"  }
 anyhow.workspace = true
 shell-escape = "0.1.5"
-owo-colors = "3.5.0"
-reqwest = { version = "0.11.17", default-features = false, features = [
-    "blocking",
-    "rustls-tls",
-] }
+owo-colors.workspace = true
 semver = "1.0.17"
-base64.workspace = true
 walkdir = "2.3.3"
 globset = "0.4.10"
 crossbeam = "0.8.2"
-tinytemplate = "1.2.1"
 chrono.workspace = true
 thiserror.workspace = true
 log.workspace = true
 env_logger.workspace = true
 which = "4.4.0"
+file-header = "0.1.2"
+serde_json.workspace = true
 
 [dev-dependencies]
 tempfile.workspace = true
diff --git a/nearby/crypto/bssl-crypto/Cargo.toml b/nearby/connections/connections_adv/connections_adv/Cargo.toml
similarity index 88%
rename from nearby/crypto/bssl-crypto/Cargo.toml
rename to nearby/connections/connections_adv/connections_adv/Cargo.toml
index bfe3964..ff17ad2 100644
--- a/nearby/crypto/bssl-crypto/Cargo.toml
+++ b/nearby/connections/connections_adv/connections_adv/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "bssl-crypto"
+name = "connections_adv"
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
diff --git a/nearby/crypto/bssl-crypto/src/lib.rs b/nearby/connections/connections_adv/connections_adv/src/lib.rs
similarity index 72%
copy from nearby/crypto/bssl-crypto/src/lib.rs
copy to nearby/connections/connections_adv/connections_adv/src/lib.rs
index 89e6968..4f7198b 100644
--- a/nearby/crypto/bssl-crypto/src/lib.rs
+++ b/nearby/connections/connections_adv/connections_adv/src/lib.rs
@@ -12,5 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Placeholder crate to satisfy cargo. If actually using boring ssl, please run the
-//! `build-boringssl` subcommand of the top level crate.
+pub fn add(left: usize, right: usize) -> usize {
+    left + right
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn it_works() {
+        let result = add(2, 2);
+        assert_eq!(result, 4);
+    }
+}
diff --git a/nearby/connections/ukey2/lock_adapter/src/spin.rs b/nearby/connections/ukey2/lock_adapter/src/spin.rs
deleted file mode 100644
index be363d4..0000000
--- a/nearby/connections/ukey2/lock_adapter/src/spin.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use crate::NoPoisonMutex;
-
-pub struct Mutex<T>(spin::Mutex<T>);
-
-impl<T> NoPoisonMutex<T> for Mutex<T> {
-    type MutexGuard<'a> = spin::MutexGuard<'a, T> where T: 'a;
-
-    fn lock(&self) -> Self::MutexGuard<'_> {
-        self.0.lock()
-    }
-
-    fn try_lock(&self) -> Option<Self::MutexGuard<'_>> {
-        self.0.try_lock()
-    }
-
-    fn new(value: T) -> Self {
-        Self(spin::Mutex::new(value))
-    }
-}
diff --git a/nearby/connections/ukey2/lock_adapter/src/std.rs b/nearby/connections/ukey2/lock_adapter/src/std.rs
deleted file mode 100644
index ffd3ecc..0000000
--- a/nearby/connections/ukey2/lock_adapter/src/std.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-use crate::NoPoisonMutex;
-
-pub struct Mutex<T>(std::sync::Mutex<T>);
-
-impl<T> NoPoisonMutex<T> for Mutex<T> {
-    type MutexGuard<'a> = std::sync::MutexGuard<'a, T> where T: 'a;
-
-    fn lock(&self) -> Self::MutexGuard<'_> {
-        self.0.lock().unwrap_or_else(|poison| poison.into_inner())
-    }
-
-    fn try_lock(&self) -> Option<Self::MutexGuard<'_>> {
-        match self.0.try_lock() {
-            Ok(guard) => Some(guard),
-            Err(std::sync::TryLockError::Poisoned(guard)) => Some(guard.into_inner()),
-            Err(std::sync::TryLockError::WouldBlock) => None,
-        }
-    }
-
-    fn new(value: T) -> Self {
-        Self(std::sync::Mutex::new(value))
-    }
-}
diff --git a/nearby/connections/ukey2/ukey2/Cargo.toml b/nearby/connections/ukey2/ukey2/Cargo.toml
index 7c10b41..afae71e 100644
--- a/nearby/connections/ukey2/ukey2/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2/Cargo.toml
@@ -4,6 +4,12 @@
 edition.workspace = true
 publish.workspace = true
 
+[features]
+default = []
+test_rustcrypto = ["crypto_provider_default/rustcrypto"]
+test_openssl = ["crypto_provider_default/openssl"]
+test_boringssl = ["crypto_provider_default/boringssl"]
+
 [dependencies]
 crypto_provider.workspace = true
 rand.workspace = true
@@ -14,8 +20,6 @@
 num-bigint = "0.4.3"
 
 [dev-dependencies]
-rand = { workspace = true, features = ["std_rng"] }
-crypto_provider_rustcrypto = {workspace = true, features = ["std"] }
-crypto_provider_openssl.workspace = true
-rstest = "0.16.0"
+rand = { workspace = true, features = ["std_rng", "getrandom"] }
+crypto_provider_default.workspace = true
 sha2.workspace = true
diff --git a/nearby/connections/ukey2/ukey2/src/tests.rs b/nearby/connections/ukey2/ukey2/src/tests.rs
index b58cee6..65274a6 100644
--- a/nearby/connections/ukey2/ukey2/src/tests.rs
+++ b/nearby/connections/ukey2/ukey2/src/tests.rs
@@ -21,42 +21,41 @@
 use crypto_provider::p256::P256;
 use crypto_provider::x25519::X25519;
 use crypto_provider::{CryptoProvider, CryptoRng};
-use crypto_provider_openssl::Openssl;
-use crypto_provider_rustcrypto::RustCrypto;
+use crypto_provider_default::CryptoProviderImpl;
 use rand::rngs::StdRng;
 use rand::{Rng, SeedableRng};
-use rstest::rstest;
 use sha2::Digest;
 use std::collections::hash_set;
 use ukey2_proto::protobuf::Message;
 use ukey2_proto::ukey2_all_proto::ukey;
 
-#[rstest]
-fn advance_from_init_to_finish_client_test<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+type X25519EphemeralSecret =
+    <<CryptoProviderImpl as CryptoProvider>::X25519 as EcdhProvider<X25519>>::EphemeralSecret;
+type X25519PublicKey =
+    <<CryptoProviderImpl as CryptoProvider>::X25519 as EcdhProvider<X25519>>::PublicKey;
+type P256EphemeralSecret =
+    <<CryptoProviderImpl as CryptoProvider>::P256 as EcdhProvider<P256>>::EphemeralSecret;
+
+#[test]
+fn advance_from_init_to_finish_client_test() {
     let mut rng = StdRng::from_entropy();
-    let client1 = Ukey2ClientStage1::<C>::from(
+    let client1 = Ukey2ClientStage1::<CryptoProviderImpl>::from(
         &mut rng,
         "next protocol".to_string(),
         HandshakeImplementation::Spec,
     );
 
     let secret =
-        <C::X25519 as EcdhProvider<X25519>>::EphemeralSecret::generate_random(
-            &mut <<C::X25519 as EcdhProvider<X25519>>::EphemeralSecret as EphemeralSecret<
-                X25519,
-            >>::Rng::new(),
-        );
-    let public_key =
-        <C::X25519 as EcdhProvider<X25519>>::PublicKey::from_bytes(&secret.public_key_bytes())
-            .unwrap();
+        X25519EphemeralSecret::generate_random(&mut <X25519EphemeralSecret as EphemeralSecret<
+            X25519,
+        >>::Rng::new());
+    let public_key = X25519PublicKey::from_bytes(secret.public_key_bytes().as_ref()).unwrap();
     let random: [u8; 32] = rng.gen();
     let message_data: ukey::Ukey2ServerInit = ukey::Ukey2ServerInit {
         version: Some(1),
         random: Some(random.to_vec()),
         handshake_cipher: Some(ukey::Ukey2HandshakeCipher::CURVE25519_SHA512.into()),
-        public_key: Some(public_key.to_bytes()),
+        public_key: Some(public_key.to_bytes().as_ref().to_vec()),
         ..Default::default()
     };
 
@@ -66,25 +65,24 @@
     // TODO assertions on client state
 }
 
-#[rstest]
-fn advance_from_init_to_complete_server_x25519_test<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn advance_from_init_to_complete_server_x25519_test() {
     let mut rng = StdRng::from_entropy();
     let mut next_protocols = hash_set::HashSet::new();
     let _ = next_protocols.insert("AES_256_CBC-HMAC_SHA256".to_string());
-    let server1 = Ukey2ServerStage1::<C>::from(next_protocols, HandshakeImplementation::Spec);
+    let server1 = Ukey2ServerStage1::<CryptoProviderImpl>::from(
+        next_protocols,
+        HandshakeImplementation::Spec,
+    );
     // We construct a ClientInit message for the server to get it into the state to handle
     // ClientFinish messages.
     let secret =
-        <C::X25519 as EcdhProvider<X25519>>::EphemeralSecret::generate_random(
-            &mut <<C::X25519 as EcdhProvider<X25519>>::EphemeralSecret as EphemeralSecret<
-                X25519,
-            >>::Rng::new(),
-        );
+        X25519EphemeralSecret::generate_random(&mut <X25519EphemeralSecret as EphemeralSecret<
+            X25519,
+        >>::Rng::new());
     let client_finished_msg = {
         let mut msg = ukey::Ukey2ClientFinished::default();
-        msg.set_public_key(secret.public_key_bytes());
+        msg.set_public_key(secret.public_key_bytes().as_ref().to_vec());
         msg.to_wrapped_msg()
     };
     let client_finished_bytes = client_finished_msg.write_to_bytes().unwrap();
@@ -120,23 +118,23 @@
     // TODO assertions on server state
 }
 
-#[rstest]
-fn advance_from_init_to_complete_server_p256_test<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn advance_from_init_to_complete_server_p256_test() {
     let mut rng = StdRng::from_entropy();
     let mut next_protocols = hash_set::HashSet::new();
     let _ = next_protocols.insert("AES_256_CBC-HMAC_SHA256".to_string());
-    let server1 = Ukey2ServerStage1::<C>::from(next_protocols, HandshakeImplementation::Spec);
+    let server1 = Ukey2ServerStage1::<CryptoProviderImpl>::from(
+        next_protocols,
+        HandshakeImplementation::Spec,
+    );
     // We construct a ClientInit message for the server to get it into the state to handle
     // ClientFinish messages.
-    let secret = <C::P256 as EcdhProvider<P256>>::EphemeralSecret::generate_random(
-        &mut <<C::P256 as EcdhProvider<P256>>::EphemeralSecret as EphemeralSecret<P256>>::Rng::new(
-        ),
+    let secret = P256EphemeralSecret::generate_random(
+        &mut <P256EphemeralSecret as EphemeralSecret<P256>>::Rng::new(),
     );
     let client_finished_msg = {
         let mut msg = ukey::Ukey2ClientFinished::default();
-        msg.set_public_key(secret.public_key_bytes());
+        msg.set_public_key(secret.public_key_bytes().as_ref().to_vec());
         msg.to_wrapped_msg()
     };
     let client_finished_bytes = client_finished_msg.write_to_bytes().unwrap();
diff --git a/nearby/connections/ukey2/ukey2/src/ukey2_handshake.rs b/nearby/connections/ukey2/ukey2/src/ukey2_handshake.rs
index a22c0ed..70db4a3 100644
--- a/nearby/connections/ukey2/ukey2/src/ukey2_handshake.rs
+++ b/nearby/connections/ukey2/ukey2/src/ukey2_handshake.rs
@@ -133,7 +133,7 @@
                 match public_key {
                     GenericPublicKey::Ec256(key) => {
                         debug_assert_eq!(cipher, HandshakeCipher::P256Sha512);
-                        Some(key.to_bytes())
+                        Some(key.to_bytes().to_vec())
                     }
                 }
             }
@@ -267,9 +267,12 @@
         server_init.set_random(random.to_vec());
         server_init.set_handshake_cipher(commitment.cipher().as_proto());
         server_init.set_public_key(match &key_pair {
-            ServerKeyPair::Curve25519(es) => es.public_key_bytes(),
+            ServerKeyPair::Curve25519(es) => es.public_key_bytes().as_ref().to_vec(),
             ServerKeyPair::P256(es) => handshake_impl
-                .encode_public_key::<C>(es.public_key_bytes(), HandshakeCipher::P256Sha512)
+                .encode_public_key::<C>(
+                    es.public_key_bytes().as_ref().to_vec(),
+                    HandshakeCipher::P256Sha512,
+                )
                 .unwrap(),
         });
 
@@ -392,7 +395,7 @@
             );
         let curve25519_client_finished_bytes = {
             let client_finished = ukey::Ukey2ClientFinished {
-                public_key: Some(curve25519_secret.public_key_bytes()),
+                public_key: Some(curve25519_secret.public_key_bytes().as_ref().to_vec()),
                 ..Default::default()
             };
             client_finished.to_wrapped_msg().write_to_bytes().unwrap()
@@ -411,7 +414,7 @@
                 public_key: Some(
                     handshake_impl
                         .encode_public_key::<C>(
-                            p256_secret.public_key_bytes(),
+                            p256_secret.public_key_bytes().as_ref().to_vec(),
                             HandshakeCipher::P256Sha512,
                         )
                         .expect("Output of p256_secret.public_key_bytes should always be valid input for encode_public_key"),
diff --git a/nearby/connections/ukey2/ukey2/tests/tests.rs b/nearby/connections/ukey2/ukey2/tests/tests.rs
index 31ba416..2f51a8c 100644
--- a/nearby/connections/ukey2/ukey2/tests/tests.rs
+++ b/nearby/connections/ukey2/ukey2/tests/tests.rs
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crypto_provider_rustcrypto::RustCrypto;
+use crypto_provider_default::CryptoProviderImpl;
 use rand::{rngs::StdRng, SeedableRng};
 use std::collections::hash_set;
 use ukey2_rs::*;
@@ -22,10 +22,12 @@
     let mut next_protocols = hash_set::HashSet::new();
     let next_protocol = "AES_256_CBC-HMAC_SHA256".to_string();
     let _ = next_protocols.insert(next_protocol.clone());
-    let server1 =
-        Ukey2ServerStage1::<RustCrypto>::from(next_protocols, HandshakeImplementation::Spec);
+    let server1 = Ukey2ServerStage1::<CryptoProviderImpl>::from(
+        next_protocols,
+        HandshakeImplementation::Spec,
+    );
     let mut rng = StdRng::from_entropy();
-    let client1 = Ukey2ClientStage1::<RustCrypto>::from(
+    let client1 = Ukey2ClientStage1::<CryptoProviderImpl>::from(
         &mut rng,
         next_protocol,
         HandshakeImplementation::Spec,
@@ -37,12 +39,18 @@
     let server3 = server2.advance_state(&mut rng, client2.client_finished_msg()).unwrap();
 
     assert_eq!(
-        server3.completed_handshake().auth_string::<RustCrypto>().derive_array::<32>(),
-        client2.completed_handshake().auth_string::<RustCrypto>().derive_array::<32>()
+        server3.completed_handshake().auth_string::<CryptoProviderImpl>().derive_array::<32>(),
+        client2.completed_handshake().auth_string::<CryptoProviderImpl>().derive_array::<32>()
     );
     assert_eq!(
-        server3.completed_handshake().next_protocol_secret::<RustCrypto>().derive_array::<32>(),
-        client2.completed_handshake().next_protocol_secret::<RustCrypto>().derive_array::<32>()
+        server3
+            .completed_handshake()
+            .next_protocol_secret::<CryptoProviderImpl>()
+            .derive_array::<32>(),
+        client2
+            .completed_handshake()
+            .next_protocol_secret::<CryptoProviderImpl>()
+            .derive_array::<32>()
     );
 }
 
@@ -51,12 +59,12 @@
     let mut next_protocols = hash_set::HashSet::new();
     let next_protocol = "AES_256_CBC-HMAC_SHA256".to_string();
     let _ = next_protocols.insert(next_protocol.clone());
-    let server1 = Ukey2ServerStage1::<RustCrypto>::from(
+    let server1 = Ukey2ServerStage1::<CryptoProviderImpl>::from(
         next_protocols,
         HandshakeImplementation::PublicKeyInProtobuf,
     );
     let mut rng = StdRng::from_entropy();
-    let client1 = Ukey2ClientStage1::<RustCrypto>::from(
+    let client1 = Ukey2ClientStage1::<CryptoProviderImpl>::from(
         &mut rng,
         next_protocol,
         HandshakeImplementation::PublicKeyInProtobuf,
@@ -68,11 +76,17 @@
     let server3 = server2.advance_state(&mut rng, client2.client_finished_msg()).unwrap();
 
     assert_eq!(
-        server3.completed_handshake().auth_string::<RustCrypto>().derive_array::<32>(),
-        client2.completed_handshake().auth_string::<RustCrypto>().derive_array::<32>()
+        server3.completed_handshake().auth_string::<CryptoProviderImpl>().derive_array::<32>(),
+        client2.completed_handshake().auth_string::<CryptoProviderImpl>().derive_array::<32>()
     );
     assert_eq!(
-        server3.completed_handshake().next_protocol_secret::<RustCrypto>().derive_array::<32>(),
-        client2.completed_handshake().next_protocol_secret::<RustCrypto>().derive_array::<32>()
+        server3
+            .completed_handshake()
+            .next_protocol_secret::<CryptoProviderImpl>()
+            .derive_array::<32>(),
+        client2
+            .completed_handshake()
+            .next_protocol_secret::<CryptoProviderImpl>()
+            .derive_array::<32>()
     );
 }
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml b/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml
index 3ca55f4..182ecf2 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_c_ffi/Cargo.toml
@@ -8,9 +8,8 @@
 ukey2_connections = { path = "../ukey2_connections" }
 ukey2_rs = { path = "../ukey2" }
 cfg-if.workspace = true
-crypto_provider_openssl = { workspace = true, optional = true }
-crypto_provider_rustcrypto = { workspace = true, optional = true, features = ["alloc"] }
-lock_adapter.workspace = true
+crypto_provider_default.workspace = true
+lock_adapter = {workspace = true, features = ["spin"]}
 
 lazy_static.workspace = true
 log.workspace = true
@@ -18,11 +17,11 @@
 rand_chacha.workspace = true
 
 [features]
-default = ["rustcrypto"]
-std = ["crypto_provider_rustcrypto/std", "lock_adapter/std"]
-openssl = ["dep:crypto_provider_openssl", "std"]
-rustcrypto = ["crypto_provider_rustcrypto"]
-crypto_provider_rustcrypto = ["dep:crypto_provider_rustcrypto"]
+default = ["rustcrypto", "std"]
+std = ["lock_adapter/std"]
+openssl = ["crypto_provider_default/openssl", "std"]
+boringssl = ["crypto_provider_default/boringssl", "std"]
+rustcrypto = ["crypto_provider_default/rustcrypto"]
 
 
 [lib]
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt b/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt
index c1247eb..165d861 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt
+++ b/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt
@@ -14,6 +14,8 @@
 
 cmake_minimum_required(VERSION 3.14)
 
+project(Ukey2)
+
 enable_testing()
 
 include_directories(
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_ffi.h b/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_ffi.h
index b269da8..654e293 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_ffi.h
+++ b/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_ffi.h
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include "ukey2_bindings.h"
+
 #include <string>
 
 struct D2DRestoreConnectionContextV1Result;
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_test.cc b/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_test.cc
index 1e913b5..962ada8 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_test.cc
+++ b/nearby/connections/ukey2/ukey2_c_ffi/cpp/ukey2_test.cc
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "ukey2_ffi.h"
+
 #include <string>
 
-#include <gtest/gtest.h>
-#include "ukey2_ffi.h"
+#include "gtest/gtest.h"
 
 namespace {
 
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs b/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs
index a79fe60..e5cc548 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs
+++ b/nearby/connections/ukey2/ukey2_c_ffi/src/lib.rs
@@ -15,6 +15,7 @@
 use std::collections::HashMap;
 use std::ptr::null_mut;
 
+use crypto_provider_default::CryptoProviderImpl as CryptoProvider;
 use lazy_static::lazy_static;
 use lock_adapter::NoPoisonMutex;
 use rand::Rng;
@@ -31,13 +32,6 @@
     InitiatorD2DHandshakeContext, ServerD2DHandshakeContext,
 };
 
-cfg_if::cfg_if! {
-    if #[cfg(feature = "rustcrypto")] {
-        use crypto_provider_rustcrypto::RustCrypto as CryptoProvider;
-    } else {
-        use crypto_provider_openssl::Openssl as CryptoProvider;
-    }
-}
 #[repr(C)]
 pub struct RustFFIByteArray {
     ptr: *mut u8,
diff --git a/nearby/connections/ukey2/ukey2_connections/Cargo.toml b/nearby/connections/ukey2/ukey2_connections/Cargo.toml
index 41fe10d..8337e96 100644
--- a/nearby/connections/ukey2/ukey2_connections/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_connections/Cargo.toml
@@ -4,6 +4,12 @@
 edition.workspace = true
 publish.workspace = true
 
+[features]
+default = []
+test_boringssl = ["crypto_provider_default/boringssl"]
+test_rustcrypto = ["crypto_provider_default/rustcrypto"]
+test_openssl = ["crypto_provider_default/openssl"]
+
 [dependencies]
 ukey2_rs = { path = "../ukey2" }
 
@@ -12,12 +18,13 @@
 ukey2_proto.workspace = true
 nom = { version = "7.1.1", features = ["alloc"] }
 bytes = "1.2.1"
-criterion = "0.4.0"
+criterion.workspace = true
 
 [dev-dependencies]
-crypto_provider_openssl.workspace = true
+crypto_provider_default.workspace = true
+# This would only be used when the feature "test_rustcrypto" is set, but optional dev-dependencies
+# are not allowed ¯\_(ツ)_/¯
 crypto_provider_rustcrypto = { workspace = true, features = ["alloc", "std"] }
-rstest = "0.16.0"
 
 [[bench]]
 name = "ukey2_benches"
diff --git a/nearby/connections/ukey2/ukey2_connections/benches/ukey2_benches.rs b/nearby/connections/ukey2/ukey2_connections/benches/ukey2_benches.rs
index b609761..e48400a 100644
--- a/nearby/connections/ukey2/ukey2_connections/benches/ukey2_benches.rs
+++ b/nearby/connections/ukey2/ukey2_connections/benches/ukey2_benches.rs
@@ -16,7 +16,7 @@
 use rand::{Rng, SeedableRng};
 
 use crypto_provider::CryptoProvider;
-use crypto_provider_rustcrypto::RustCrypto;
+use crypto_provider_default::CryptoProviderImpl;
 use ukey2_connections::{
     D2DConnectionContextV1, D2DHandshakeContext, InitiatorD2DHandshakeContext,
     ServerD2DHandshakeContext,
@@ -63,16 +63,18 @@
     let mut group = c.benchmark_group("throughput");
     let mut plaintext = Vec::new();
     let (mut initiator_ctx, mut server_ctx) =
-        run_handshake_with_rng::<RustCrypto, _>(rand::rngs::StdRng::from_entropy());
+        run_handshake_with_rng::<CryptoProviderImpl, _>(rand::rngs::StdRng::from_entropy());
     for len in [10 * kib, 1024 * kib] {
         group.throughput(Throughput::Bytes(len as u64));
         plaintext.resize(len, 0);
         rand::thread_rng().fill(&mut plaintext[..]);
         group.bench_function(format!("UKEY2 encrypt/decrypt {}KiB", len / kib), |b| {
             b.iter(|| {
-                let msg =
-                    initiator_ctx.encode_message_to_peer::<RustCrypto, &[u8]>(&plaintext, None);
-                black_box(server_ctx.decode_message_from_peer::<RustCrypto, &[u8]>(&msg, None))
+                let msg = initiator_ctx
+                    .encode_message_to_peer::<CryptoProviderImpl, &[u8]>(&plaintext, None);
+                black_box(
+                    server_ctx.decode_message_from_peer::<CryptoProviderImpl, &[u8]>(&msg, None),
+                )
             })
         });
     }
diff --git a/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.lock b/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.lock
index 4a00416..3f72ae0 100644
--- a/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.lock
+++ b/nearby/connections/ukey2/ukey2_connections/fuzz/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -336,6 +350,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_rustcrypto"
@@ -343,6 +360,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -372,9 +390,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -451,9 +469,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -555,6 +573,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1233,6 +1261,12 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
 name = "typenum"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1495,9 +1529,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/connections/ukey2/ukey2_connections/src/tests.rs b/nearby/connections/ukey2/ukey2_connections/src/tests.rs
index 35a1e70..32a6b28 100644
--- a/nearby/connections/ukey2/ukey2_connections/src/tests.rs
+++ b/nearby/connections/ukey2/ukey2_connections/src/tests.rs
@@ -12,14 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crypto_provider_rustcrypto::RustCryptoImpl;
+use crypto_provider::CryptoProvider;
+use crypto_provider_default::CryptoProviderImpl;
 use rand::SeedableRng;
 use rand::{rngs::StdRng, CryptoRng, RngCore};
-use rstest::rstest;
-
-use crypto_provider::CryptoProvider;
-use crypto_provider_openssl::Openssl;
-use crypto_provider_rustcrypto::RustCrypto;
 use ukey2_rs::HandshakeImplementation;
 
 use crate::{
@@ -28,27 +24,25 @@
     InitiatorD2DHandshakeContext, ServerD2DHandshakeContext,
 };
 
-#[rstest]
-fn crypto_test_encrypt_decrypt<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+type AesCbcPkcs7Padded = <CryptoProviderImpl as CryptoProvider>::AesCbcPkcs7Padded;
+
+#[test]
+fn crypto_test_encrypt_decrypt() {
     let message = b"Hello World!";
     let key = b"42424242424242424242424242424242";
     let (ciphertext, iv) =
-        encrypt::<_, C::AesCbcPkcs7Padded>(key, message, &mut rand::rngs::StdRng::from_entropy());
-    let decrypt_result = decrypt::<C::AesCbcPkcs7Padded>(key, ciphertext.as_slice(), &iv);
+        encrypt::<_, AesCbcPkcs7Padded>(key, message, &mut rand::rngs::StdRng::from_entropy());
+    let decrypt_result = decrypt::<AesCbcPkcs7Padded>(key, ciphertext.as_slice(), &iv);
     let ptext = decrypt_result.expect("Decrypt should be successful");
     assert_eq!(ptext, message.to_vec());
 }
 
-#[rstest]
-fn crypto_test_encrypt_seeded<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn crypto_test_encrypt_seeded() {
     let message = b"Hello World!";
     let key = b"42424242424242424242424242424242";
     let mut rng = MockRng;
-    let (ciphertext, iv) = encrypt::<_, C::AesCbcPkcs7Padded>(key, message, &mut rng);
+    let (ciphertext, iv) = encrypt::<_, AesCbcPkcs7Padded>(key, message, &mut rng);
     // Expected values extracted from the results of the current implementation.
     // This test makes sure that we don't accidentally change the encryption logic that
     // causes incompatibility between versions.
@@ -59,43 +53,36 @@
     );
 }
 
-#[rstest]
-fn crypto_test_decrypt_seeded<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn crypto_test_decrypt_seeded() {
     let iv = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
     let ciphertext = [20, 59, 195, 101, 11, 208, 245, 128, 247, 196, 81, 80, 158, 77, 174, 61];
     let key = b"42424242424242424242424242424242";
-    let plaintext = decrypt::<C::AesCbcPkcs7Padded>(key, &ciphertext, &iv).unwrap();
+    let plaintext = decrypt::<AesCbcPkcs7Padded>(key, &ciphertext, &iv).unwrap();
     assert_eq!(plaintext, b"Hello World!");
 }
 
-#[rstest]
-fn decrypt_test_wrong_key<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn decrypt_test_wrong_key() {
     let message = b"Hello World!";
     let good_key = b"42424242424242424242424242424242";
-    let (ciphertext, iv) = encrypt::<_, C::AesCbcPkcs7Padded>(
-        good_key,
-        message,
-        &mut rand::rngs::StdRng::from_entropy(),
-    );
+    let (ciphertext, iv) =
+        encrypt::<_, AesCbcPkcs7Padded>(good_key, message, &mut rand::rngs::StdRng::from_entropy());
     let bad_key = b"43434343434343434343434343434343";
-    let decrypt_result = decrypt::<C::AesCbcPkcs7Padded>(bad_key, ciphertext.as_slice(), &iv);
+    let decrypt_result = decrypt::<AesCbcPkcs7Padded>(bad_key, ciphertext.as_slice(), &iv);
     match decrypt_result {
         // The padding is valid, but the decrypted value should be bad since the keys don't match
         Ok(decrypted_bad) => assert_ne!(decrypted_bad, message),
         // The padding is bad, so it returns an error and is unable to decrypt
         Err(crypto_provider::aes::cbc::DecryptionError::BadPadding) => (),
     }
-    let decrypt_result = decrypt::<C::AesCbcPkcs7Padded>(good_key, ciphertext.as_slice(), &iv);
+    let decrypt_result = decrypt::<AesCbcPkcs7Padded>(good_key, ciphertext.as_slice(), &iv);
     let ptext = decrypt_result.unwrap();
     assert_eq!(ptext, message.to_vec());
 }
 
-fn run_handshake<C: CryptoProvider>() -> (D2DConnectionContextV1, D2DConnectionContextV1) {
-    run_handshake_with_rng::<C, _>(rand::rngs::StdRng::from_entropy())
+fn run_handshake() -> (D2DConnectionContextV1, D2DConnectionContextV1) {
+    run_handshake_with_rng::<CryptoProviderImpl, _>(rand::rngs::StdRng::from_entropy())
 }
 
 fn run_handshake_with_rng<C, R>(
@@ -133,15 +120,17 @@
     (initiator_ctx.to_connection_context().unwrap(), server_ctx.to_connection_context().unwrap())
 }
 
-#[rstest]
-fn send_receive_message_seeded<C: CryptoProvider>(
-    // TODO: Find a way to inject RNG / generated ephemeral secrets in openSSL and test them here
-    #[values(RustCryptoImpl::< MockRng >::new())] _crypto_provider: C,
-) {
+// TODO: Find a way to inject RNG / generated ephemeral secrets in openSSL and test them here
+#[cfg(feature = "test_rustcrypto")]
+#[test]
+fn send_receive_message_seeded() {
+    use crypto_provider_rustcrypto::RustCryptoImpl;
     let rng = MockRng;
     let message = b"Hello World!";
-    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake_with_rng::<C, _>(rng);
-    let encoded = init_conn_ctx.encode_message_to_peer::<C, &[u8]>(message, None);
+    let (mut init_conn_ctx, mut server_conn_ctx) =
+        run_handshake_with_rng::<RustCryptoImpl<MockRng>, _>(rng);
+    let encoded =
+        init_conn_ctx.encode_message_to_peer::<RustCryptoImpl<MockRng>, &[u8]>(message, None);
     // Expected values extracted from the results of the current implementation.
     // This test makes sure that we don't accidentally change the encryption logic that
     // causes incompatibility between versions.
@@ -155,102 +144,104 @@
             45, 239, 234, 248, 148, 9, 150, 204, 117, 32, 216, 5, 126, 224, 39
         ]
     );
-    let decoded = server_conn_ctx.decode_message_from_peer::<C, &[u8]>(&encoded, None).unwrap();
+    let decoded = server_conn_ctx
+        .decode_message_from_peer::<CryptoProviderImpl, &[u8]>(&encoded, None)
+        .unwrap();
     assert_eq!(message, &decoded[..]);
 }
 
-#[rstest]
-fn send_receive_message<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn send_receive_message() {
     let message = b"Hello World!";
-    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake::<C>();
-    let encoded = init_conn_ctx.encode_message_to_peer::<C, &[u8]>(message, None);
-    let decoded = server_conn_ctx.decode_message_from_peer::<C, &[u8]>(encoded.as_slice(), None);
+    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake();
+    let encoded = init_conn_ctx.encode_message_to_peer::<CryptoProviderImpl, &[u8]>(message, None);
+    let decoded = server_conn_ctx
+        .decode_message_from_peer::<CryptoProviderImpl, &[u8]>(encoded.as_slice(), None);
     assert_eq!(message.to_vec(), decoded.expect("Decode should be successful"));
 }
 
-#[rstest]
-fn send_receive_message_associated_data<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
+#[test]
+fn send_receive_message_associated_data() {
     let message = b"Hello World!";
-    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake::<C>();
-    let encoded = init_conn_ctx.encode_message_to_peer::<C, _>(message, Some(b"associated data"));
-    let decoded = server_conn_ctx
-        .decode_message_from_peer::<C, _>(encoded.as_slice(), Some(b"associated data"));
+    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake();
+    let encoded = init_conn_ctx
+        .encode_message_to_peer::<CryptoProviderImpl, _>(message, Some(b"associated data"));
+    let decoded = server_conn_ctx.decode_message_from_peer::<CryptoProviderImpl, _>(
+        encoded.as_slice(),
+        Some(b"associated data"),
+    );
     assert_eq!(message.to_vec(), decoded.expect("Decode should be successful"));
     // Make sure decode fails with missing associated data.
-    let decoded = server_conn_ctx.decode_message_from_peer::<C, &[u8]>(encoded.as_slice(), None);
+    let decoded = server_conn_ctx
+        .decode_message_from_peer::<CryptoProviderImpl, &[u8]>(encoded.as_slice(), None);
     assert!(decoded.is_err());
     // Make sure decode fails with different associated data.
-    let decoded = server_conn_ctx
-        .decode_message_from_peer::<C, _>(encoded.as_slice(), Some(b"assoc1ated data"));
+    let decoded = server_conn_ctx.decode_message_from_peer::<CryptoProviderImpl, _>(
+        encoded.as_slice(),
+        Some(b"assoc1ated data"),
+    );
     assert!(decoded.is_err());
 }
 
-#[rstest]
-fn test_save_restore_session<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
-    let (init_conn_ctx, server_conn_ctx) = run_handshake::<C>();
+#[test]
+fn test_save_restore_session() {
+    let (init_conn_ctx, server_conn_ctx) = run_handshake();
     let init_session = init_conn_ctx.save_session();
     let server_session = server_conn_ctx.save_session();
     let mut init_restored_ctx =
-        D2DConnectionContextV1::from_saved_session::<C>(init_session.as_slice())
+        D2DConnectionContextV1::from_saved_session::<CryptoProviderImpl>(init_session.as_slice())
             .expect("failed to restore client session");
     let mut server_restored_ctx =
-        D2DConnectionContextV1::from_saved_session::<C>(server_session.as_slice())
+        D2DConnectionContextV1::from_saved_session::<CryptoProviderImpl>(server_session.as_slice())
             .expect("failed to restore server session");
     let message = b"Hello World!";
-    let encoded = init_restored_ctx.encode_message_to_peer::<C, &[u8]>(message, None);
-    let decoded =
-        server_restored_ctx.decode_message_from_peer::<C, &[u8]>(encoded.as_slice(), None);
+    let encoded =
+        init_restored_ctx.encode_message_to_peer::<CryptoProviderImpl, &[u8]>(message, None);
+    let decoded = server_restored_ctx
+        .decode_message_from_peer::<CryptoProviderImpl, &[u8]>(encoded.as_slice(), None);
     assert_eq!(message.to_vec(), decoded.expect("Decode should be successful"));
 }
 
-#[rstest]
-fn test_save_restore_bad_session<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
-    let (init_conn_ctx, server_conn_ctx) = run_handshake::<C>();
+#[test]
+fn test_save_restore_bad_session() {
+    let (init_conn_ctx, server_conn_ctx) = run_handshake();
     let init_session = init_conn_ctx.save_session();
     let server_session = server_conn_ctx.save_session();
-    let _ = D2DConnectionContextV1::from_saved_session::<C>(init_session.as_slice())
-        .expect("failed to restore client session");
+    let _ =
+        D2DConnectionContextV1::from_saved_session::<CryptoProviderImpl>(init_session.as_slice())
+            .expect("failed to restore client session");
     let server_restored_ctx =
-        D2DConnectionContextV1::from_saved_session::<C>(&server_session[0..60]);
+        D2DConnectionContextV1::from_saved_session::<CryptoProviderImpl>(&server_session[0..60]);
     assert_eq!(server_restored_ctx.unwrap_err(), DeserializeError::BadDataLength);
 }
 
-#[rstest]
-fn test_save_restore_bad_protocol_version<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
-    let (init_conn_ctx, server_conn_ctx) = run_handshake::<C>();
+#[test]
+fn test_save_restore_bad_protocol_version() {
+    let (init_conn_ctx, server_conn_ctx) = run_handshake();
     let init_session = init_conn_ctx.save_session();
     let mut server_session = server_conn_ctx.save_session();
-    let _ = D2DConnectionContextV1::from_saved_session::<C>(init_session.as_slice())
-        .expect("failed to restore client session");
+    let _ =
+        D2DConnectionContextV1::from_saved_session::<CryptoProviderImpl>(init_session.as_slice())
+            .expect("failed to restore client session");
     server_session[0] = 0; // Change the protocol version to an invalid one (0)
-    let server_restored_ctx = D2DConnectionContextV1::from_saved_session::<C>(&server_session);
+    let server_restored_ctx =
+        D2DConnectionContextV1::from_saved_session::<CryptoProviderImpl>(&server_session);
     assert_eq!(server_restored_ctx.unwrap_err(), DeserializeError::BadProtocolVersion);
 }
 
-#[rstest]
-fn test_unique_session<C: CryptoProvider>(
-    #[values(RustCrypto::new(), Openssl)] _crypto_provider: C,
-) {
-    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake::<C>();
-    let init_session = init_conn_ctx.get_session_unique::<C>();
-    let server_session = server_conn_ctx.get_session_unique::<C>();
+#[test]
+fn test_unique_session() {
+    let (mut init_conn_ctx, mut server_conn_ctx) = run_handshake();
+    let init_session = init_conn_ctx.get_session_unique::<CryptoProviderImpl>();
+    let server_session = server_conn_ctx.get_session_unique::<CryptoProviderImpl>();
     let message = b"Hello World!";
-    let encoded = init_conn_ctx.encode_message_to_peer::<C, &[u8]>(message, None);
-    let decoded = server_conn_ctx.decode_message_from_peer::<C, &[u8]>(encoded.as_slice(), None);
+    let encoded = init_conn_ctx.encode_message_to_peer::<CryptoProviderImpl, &[u8]>(message, None);
+    let decoded = server_conn_ctx
+        .decode_message_from_peer::<CryptoProviderImpl, &[u8]>(encoded.as_slice(), None);
     assert_eq!(message.to_vec(), decoded.expect("Decode should be successful"));
-    let init_session_after = init_conn_ctx.get_session_unique::<C>();
-    let server_session_after = server_conn_ctx.get_session_unique::<C>();
-    let bad_server_ctx = D2DConnectionContextV1::new::<C>(
+    let init_session_after = init_conn_ctx.get_session_unique::<CryptoProviderImpl>();
+    let server_session_after = server_conn_ctx.get_session_unique::<CryptoProviderImpl>();
+    let bad_server_ctx = D2DConnectionContextV1::new::<CryptoProviderImpl>(
         server_conn_ctx.get_sequence_number_for_decoding(),
         server_conn_ctx.get_sequence_number_for_encoding(),
         Aes256Key::default(),
@@ -260,7 +251,7 @@
     assert_eq!(init_session, init_session_after);
     assert_eq!(server_session, server_session_after);
     assert_eq!(init_session, server_session);
-    assert_ne!(server_session, bad_server_ctx.get_session_unique::<C>());
+    assert_ne!(server_session, bad_server_ctx.get_session_unique::<CryptoProviderImpl>());
 }
 
 #[test]
diff --git a/nearby/connections/ukey2/ukey2_jni/Cargo.toml b/nearby/connections/ukey2/ukey2_jni/Cargo.toml
index 7a399d3..4852444 100644
--- a/nearby/connections/ukey2/ukey2_jni/Cargo.toml
+++ b/nearby/connections/ukey2/ukey2_jni/Cargo.toml
@@ -9,11 +9,10 @@
 [dependencies]
 ukey2_connections = { path = "../ukey2_connections" }
 ukey2_rs = { path = "../ukey2" }
-lock_adapter.workspace = true
+lock_adapter = {workspace = true, features = ["spin"]}
 
 cfg-if.workspace = true
-crypto_provider_openssl = { workspace = true, optional = true }
-crypto_provider_rustcrypto = { workspace = true, optional = true, features = [ "alloc" ] }
+crypto_provider_default = { workspace = true }
 lazy_static.workspace = true
 rand.workspace = true
 rand_chacha.workspace = true
@@ -22,10 +21,10 @@
 
 [features]
 default = ["rustcrypto"]
-openssl = ["dep:crypto_provider_openssl", "std"]
-rustcrypto = ["crypto_provider_rustcrypto"]
-std = ["lock_adapter/std", "crypto_provider_rustcrypto/std"]
-crypto_provider_rustcrypto = ["dep:crypto_provider_rustcrypto"]
+openssl = ["crypto_provider_default/openssl", "std"]
+rustcrypto = ["crypto_provider_default/rustcrypto"]
+boringssl = ["crypto_provider_default/boringssl"]
+std = ["lock_adapter/std"]
 
 [lib]
 crate_type = ["cdylib"]
\ No newline at end of file
diff --git a/nearby/connections/ukey2/ukey2_jni/src/lib.rs b/nearby/connections/ukey2/ukey2_jni/src/lib.rs
index 570e74b..82d317e 100644
--- a/nearby/connections/ukey2/ukey2_jni/src/lib.rs
+++ b/nearby/connections/ukey2/ukey2_jni/src/lib.rs
@@ -34,13 +34,7 @@
     ServerD2DHandshakeContext,
 };
 
-cfg_if::cfg_if! {
-    if #[cfg(feature = "rustcrypto")] {
-        use crypto_provider_rustcrypto::RustCrypto as CryptoProvider;
-    } else {
-        use crypto_provider_openssl::Openssl as CryptoProvider;
-    }
-}
+use crypto_provider_default::CryptoProviderImpl as CryptoProvider;
 // Handle management
 
 type D2DBox = Box<dyn D2DHandshakeContext>;
diff --git a/nearby/crypto/crypto_provider/Cargo.toml b/nearby/crypto/crypto_provider/Cargo.toml
index 60f8626..e7dd8ba 100644
--- a/nearby/crypto/crypto_provider/Cargo.toml
+++ b/nearby/crypto/crypto_provider/Cargo.toml
@@ -4,11 +4,14 @@
 edition.workspace = true
 publish.workspace = true
 
+[dependencies]
+tinyvec.workspace = true
+
 [dev-dependencies]
 criterion.workspace = true
 hex-literal.workspace = true
 crypto_provider_openssl.workspace = true
-crypto_provider_rustcrypto.workspace = true
+crypto_provider_rustcrypto = { workspace = true, features = ["std"] }
 rand_ext.workspace = true
 rand.workspace = true
 
@@ -17,6 +20,7 @@
 std = []
 alloc = []
 test_vectors = []
+raw_private_key_permit = []
 
 [[bench]]
 name = "hmac_bench"
diff --git a/nearby/crypto/crypto_provider/src/aead.rs b/nearby/crypto/crypto_provider/src/aead.rs
new file mode 100644
index 0000000..165272e
--- /dev/null
+++ b/nearby/crypto/crypto_provider/src/aead.rs
@@ -0,0 +1,86 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+
+/// An implementation of AES-GCM-SIV.
+///
+/// An AesGcmSiv impl may be used for encryption and decryption.
+pub trait AesGcmSiv: Aead<Nonce = [u8; 12]> {}
+
+/// An implementation of AES-GCM.
+///
+/// An AesGcm impl may be used for encryption and decryption.
+pub trait AesGcm: Aead<Nonce = [u8; 12]> {}
+
+/// Error returned on unsuccessful AEAD operation.
+#[derive(Debug)]
+pub struct AeadError;
+
+/// Initializes an AEAD
+pub trait AeadInit<K: crate::aes::AesKey> {
+    /// Instantiates a new instance of the AEAD from key material.
+    fn new(key: &K) -> Self;
+}
+
+/// Authenticated Encryption with Associated Data (AEAD) algorithm, where `N` is the size of the
+/// Nonce. Encrypts and decrypts buffers in-place.
+pub trait Aead {
+    /// The size of the authentication tag, this is appended to the message on the encrypt operation
+    /// and truncated from the plaintext after decrypting.
+    const TAG_SIZE: usize;
+
+    /// The cryptographic nonce used by the AEAD. The nonce must be unique for all messages with
+    /// the same key. This is critically important - nonce reuse may completely undermine the
+    /// security of the AEAD. Nonces may be predictable and public, so long as they are unique.
+    type Nonce: AsRef<[u8]>;
+
+    /// The type of the tag, which should always be [u8; Self::TAG_SIZE].
+    type Tag: AsRef<[u8]>;
+
+    /// Encrypt the given buffer containing a plaintext message. On success returns the encrypted
+    /// `msg` and appended auth tag, which will result in a Vec which is  `Self::TAG_SIZE` bytes
+    /// greater than the initial message.
+    #[cfg(feature = "alloc")]
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError>;
+
+    /// Encrypt the given buffer containing a plaintext message in-place, and returns the tag in the
+    /// result value.
+    fn encrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError>;
+
+    /// Decrypt the message, returning the decrypted plaintext or an error in the event the
+    /// provided authentication tag does not match the given ciphertext. On success the returned
+    /// Vec will only contain the plaintext and so will be `Self::TAG_SIZE` bytes less than the
+    /// initial message.
+    #[cfg(feature = "alloc")]
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError>;
+
+    /// Decrypt the message in-place, returning an error and leaving the input `msg` unchanged in
+    /// the event the provided authentication tag does not match the given ciphertext.
+    fn decrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+        tag: &Self::Tag,
+    ) -> Result<(), AeadError>;
+}
diff --git a/nearby/crypto/crypto_provider/src/aead/aes_gcm_siv.rs b/nearby/crypto/crypto_provider/src/aead/aes_gcm_siv.rs
deleted file mode 100644
index 3be7db3..0000000
--- a/nearby/crypto/crypto_provider/src/aead/aes_gcm_siv.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Traits for AES-GCM-SIV.
-
-extern crate alloc;
-use crate::aead::Aead;
-
-/// An implementation of AES-GCM-SIV.
-///
-/// An AesGcmSiv impl may be used for encryption and decryption.
-pub trait AesGcmSiv: Aead<Nonce = [u8; 12]> {}
diff --git a/nearby/crypto/crypto_provider/src/aead/mod.rs b/nearby/crypto/crypto_provider/src/aead/mod.rs
deleted file mode 100644
index 27284a9..0000000
--- a/nearby/crypto/crypto_provider/src/aead/mod.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-extern crate alloc;
-use alloc::vec::Vec;
-
-/// Contains traits for the AES-GCM-SIV AEAD algorithm.
-pub mod aes_gcm_siv;
-
-/// Error returned on unsuccessful AEAD operation.
-pub struct AeadError;
-
-/// Authenticated Encryption with Associated Data (AEAD) algorithm, where `N` is the size of the
-/// Nonce. Encrypts and decrypts buffers in-place.
-pub trait Aead {
-    /// The size of the authentication tag, this is appended to the message on the encrypt operation
-    /// and truncated from the plaintext after decrypting.
-    const TAG_SIZE: usize;
-
-    /// The cryptographic nonce used by the AEAD. The nonce must be unique for all messages with
-    /// the same key. This is critically important - nonce reuse may completely undermine the
-    /// security of the AEAD. Nonces may be predictable and public, so long as they are unique.
-    type Nonce;
-
-    /// The key material used to initialize the AEAD.
-    type Key;
-
-    /// Instantiates a new instance of the AEAD from key material.
-    fn new(key: &Self::Key) -> Self;
-
-    /// Encrypt the given buffer containing a plaintext message in-place. On success increases the
-    /// buffer by `Self::TAG_SIZE` bytes and appends the auth tag to the end of `msg`.
-    fn encrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &Self::Nonce) -> Result<(), AeadError>;
-
-    /// Decrypt the message in-place, returning an error in the event the provided authentication
-    /// tag does not match the given ciphertext. The buffer will be truncated to the length of the
-    /// original plaintext message upon success.
-    fn decrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &Self::Nonce) -> Result<(), AeadError>;
-}
diff --git a/nearby/crypto/crypto_provider/src/aes/cbc.rs b/nearby/crypto/crypto_provider/src/aes/cbc.rs
index e32c588..59ce52d 100644
--- a/nearby/crypto/crypto_provider/src/aes/cbc.rs
+++ b/nearby/crypto/crypto_provider/src/aes/cbc.rs
@@ -14,7 +14,10 @@
 
 //! Traits for AES-CBC 256 with PKCS7 padding.
 
+#[cfg(feature = "alloc")]
 extern crate alloc;
+use crate::tinyvec::SliceVec;
+#[cfg(feature = "alloc")]
 use alloc::vec::Vec;
 
 use super::Aes256Key;
@@ -24,15 +27,53 @@
 
 /// Trait for implementing AES-CBC with PKCS7 padding.
 pub trait AesCbcPkcs7Padded {
+    /// Calculate the padded output length (e.g. output of `encrypt`) from the unpadded length (e.g.
+    /// input message of `encrypt`).
+    fn padded_output_len(unpadded_len: usize) -> usize {
+        (unpadded_len - (unpadded_len % 16))
+            .checked_add(16)
+            .expect("Padded output length is larger than usize::MAX")
+    }
+
     /// Encrypt message using `key` and `iv`, returning a ciphertext.
+    #[cfg(feature = "alloc")]
     fn encrypt(key: &Aes256Key, iv: &AesCbcIv, message: &[u8]) -> Vec<u8>;
+
+    /// Encrypt message using `key` and `iv` in-place in the given `message` vec. The given slice
+    /// vec should have enough capacity to contain both the ciphertext and the padding (which can be
+    /// calculated from `padded_output_len()`). If it doesn't have enough capacity, an error will be
+    /// returned. The contents of the input `message` buffer is undefined in that case.
+    fn encrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        message: &mut SliceVec<u8>,
+    ) -> Result<(), EncryptionError>;
+
     /// Decrypt ciphertext using `key` and `iv`, returning the original message if `Ok()` otherwise
     /// a `DecryptionError` indicating the type of error that occurred while decrypting.
+    #[cfg(feature = "alloc")]
     fn decrypt(
         key: &Aes256Key,
         iv: &AesCbcIv,
         ciphertext: &[u8],
     ) -> Result<Vec<u8>, DecryptionError>;
+
+    /// Decrypt ciphertext using `key` and `iv` and unpad it in-place. Returning the original
+    /// message if `Ok()` otherwise a `DecryptionError` indicating the type of error that occurred
+    /// while decrypting. In that case, the contents of the `ciphertext` buffer is undefined.
+    fn decrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &mut SliceVec<u8>,
+    ) -> Result<(), DecryptionError>;
+}
+
+/// Error type for describing what went wrong encrypting a message.
+#[derive(Debug, PartialEq, Eq)]
+pub enum EncryptionError {
+    /// Failed to add PKCS7 padding to the output when encrypting a message. This typically happens
+    /// when the given output buffer does not have enough capacity to append the padding.
+    PaddingFailed,
 }
 
 /// Error type for describing what went wrong decrypting a ciphertext.
@@ -43,3 +84,65 @@
     /// correctly. Exposing padding errors can cause a padding oracle vulnerability.
     BadPadding,
 }
+
+#[cfg(test)]
+mod test {
+    #[cfg(feature = "alloc")]
+    extern crate alloc;
+    #[cfg(feature = "alloc")]
+    use alloc::vec::Vec;
+
+    use crate::aes::Aes256Key;
+    use crate::tinyvec::SliceVec;
+
+    use super::{AesCbcIv, AesCbcPkcs7Padded, DecryptionError, EncryptionError};
+
+    #[test]
+    fn test_padded_output_len() {
+        assert_eq!(AesCbcPkcs7PaddedStub::padded_output_len(0), 16);
+        assert_eq!(AesCbcPkcs7PaddedStub::padded_output_len(15), 16);
+        assert_eq!(AesCbcPkcs7PaddedStub::padded_output_len(16), 32);
+        assert_eq!(AesCbcPkcs7PaddedStub::padded_output_len(30), 32);
+        assert_eq!(AesCbcPkcs7PaddedStub::padded_output_len(32), 48);
+    }
+
+    #[test]
+    #[should_panic]
+    fn test_padded_output_len_overflow() {
+        AesCbcPkcs7PaddedStub::padded_output_len(usize::MAX);
+    }
+
+    struct AesCbcPkcs7PaddedStub;
+
+    impl AesCbcPkcs7Padded for AesCbcPkcs7PaddedStub {
+        #[cfg(feature = "alloc")]
+        fn encrypt(_key: &Aes256Key, _iv: &AesCbcIv, _message: &[u8]) -> Vec<u8> {
+            unimplemented!()
+        }
+
+        fn encrypt_in_place(
+            _key: &Aes256Key,
+            _iv: &AesCbcIv,
+            _message: &mut SliceVec<u8>,
+        ) -> Result<(), EncryptionError> {
+            unimplemented!()
+        }
+
+        #[cfg(feature = "alloc")]
+        fn decrypt(
+            _key: &Aes256Key,
+            _iv: &AesCbcIv,
+            _ciphertext: &[u8],
+        ) -> Result<Vec<u8>, DecryptionError> {
+            unimplemented!()
+        }
+
+        fn decrypt_in_place(
+            _key: &Aes256Key,
+            _iv: &AesCbcIv,
+            _ciphertext: &mut SliceVec<u8>,
+        ) -> Result<(), DecryptionError> {
+            unimplemented!()
+        }
+    }
+}
diff --git a/nearby/crypto/crypto_provider/src/aes/ctr.rs b/nearby/crypto/crypto_provider/src/aes/ctr.rs
index 73d4f55..6779fe8 100644
--- a/nearby/crypto/crypto_provider/src/aes/ctr.rs
+++ b/nearby/crypto/crypto_provider/src/aes/ctr.rs
@@ -36,10 +36,8 @@
     /// Build a `Self` from key material.
     fn new(key: &Self::Key, nonce_and_counter: NonceAndCounter) -> Self;
 
-    /// Encrypt the data in place, advancing the counter state appropriately.
-    fn encrypt(&mut self, data: &mut [u8]);
-    /// Decrypt the data in place, advancing the counter state appropriately.
-    fn decrypt(&mut self, data: &mut [u8]);
+    /// Applies the key stream to the data in place, advancing the counter state appropriately.
+    fn apply_keystream(&mut self, data: &mut [u8]);
 }
 
 /// The combined nonce and counter that CTR increments and encrypts to form the keystream.
diff --git a/nearby/crypto/crypto_provider/src/aes/mod.rs b/nearby/crypto/crypto_provider/src/aes/mod.rs
index 83e48de..e359bad 100644
--- a/nearby/crypto/crypto_provider/src/aes/mod.rs
+++ b/nearby/crypto/crypto_provider/src/aes/mod.rs
@@ -20,7 +20,6 @@
 
 pub mod ctr;
 
-#[cfg(feature = "alloc")]
 pub mod cbc;
 
 /// Block size in bytes for AES (and XTS-AES)
diff --git a/nearby/crypto/crypto_provider/src/ed25519.rs b/nearby/crypto/crypto_provider/src/ed25519.rs
index 48c2c50..b435ca2 100644
--- a/nearby/crypto/crypto_provider/src/ed25519.rs
+++ b/nearby/crypto/crypto_provider/src/ed25519.rs
@@ -43,6 +43,59 @@
 /// A byte buffer the size of a ed25519 `PrivateKey`.
 pub type RawPrivateKey = [u8; PRIVATE_KEY_LENGTH];
 
+/// A permission token which may be supplied to methods which allow
+/// converting private keys to/from raw bytes.
+///
+/// In general, operations of this kind should only be done in
+/// development-tools, tests, or in credential storage layers
+/// to prevent accidental exposure of the private key.
+pub struct RawPrivateKeyPermit {
+    _marker: (),
+}
+
+impl RawPrivateKeyPermit {
+    pub(crate) fn new() -> Self {
+        Self { _marker: () }
+    }
+}
+
+#[cfg(feature = "raw_private_key_permit")]
+impl core::default::Default for RawPrivateKeyPermit {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// A crypto-provider-independent representation of the private
+/// key of an ed25519 key-pair, kept in such a way that
+/// it does not permit de-structuring it into raw bytes,
+/// nor constructing one from raw bytes.
+///
+/// Useful for when you want a data-structure to be
+/// crypto-provider independent and contain a private key.
+#[derive(Clone)]
+pub struct PrivateKey {
+    wrapped: RawPrivateKey,
+}
+
+impl PrivateKey {
+    /// Derives the public key corresponding to this private key.
+    pub fn derive_public_key<E: Ed25519Provider>(&self) -> E::PublicKey {
+        let key_pair = E::KeyPair::from_private_key(self);
+        key_pair.public()
+    }
+    /// Returns the raw bytes of this private key.
+    /// This operation is only possible while holding a [`RawPrivateKeyPermit`].
+    pub fn raw_private_key(&self, _permit: &RawPrivateKeyPermit) -> RawPrivateKey {
+        self.wrapped
+    }
+    /// Constructs a private key from the raw bytes of the key.
+    /// This operation is only possible while holding a [`RawPrivateKeyPermit`].
+    pub fn from_raw_private_key(wrapped: RawPrivateKey, _permit: &RawPrivateKeyPermit) -> Self {
+        Self { wrapped }
+    }
+}
+
 /// The keypair which includes both public and secret halves of an asymmetric key.
 pub trait KeyPair: Sized {
     /// The ed25519 public key, used when verifying a message
@@ -52,15 +105,37 @@
     type Signature: Signature;
 
     /// Returns the private key bytes of the `KeyPair`.
-    /// This method should only ever be called by code which securely stores private credentials.
-    fn private_key(&self) -> RawPrivateKey;
+    /// This operation is only possible while holding a [`RawPrivateKeyPermit`].
+    fn raw_private_key(&self, _permit: &RawPrivateKeyPermit) -> RawPrivateKey;
 
     /// Builds a key-pair from a `RawPrivateKey` array of bytes.
-    /// This should only ever be called by code which securely stores private credentials.
-    fn from_private_key(bytes: &RawPrivateKey) -> Self
+    /// This operation is only possible while holding a [`RawPrivateKeyPermit`].
+    fn from_raw_private_key(bytes: &RawPrivateKey, _permit: &RawPrivateKeyPermit) -> Self
     where
         Self: Sized;
 
+    /// Returns the private key of the `KeyPair` in an opaque form.
+    fn private_key(&self) -> PrivateKey {
+        // We're okay to reach in and grab the bytes of the private key,
+        // since the way that we're exposing it would require a valid
+        // [`RawPrivateKeyPermit`] to extract them again.
+        let wrapped = self.raw_private_key(&RawPrivateKeyPermit::new());
+        PrivateKey { wrapped }
+    }
+
+    /// Builds a key-pair from a [`PrivateKey`], given in an opaque form.
+    fn from_private_key(private_key: &PrivateKey) -> Self
+    where
+        Self: Sized,
+    {
+        // We're okay to reach in and construct an instance from
+        // the bytes of the private key, since the way that they
+        // were originally expressed would still require a valid
+        // [`RawPrivateKeyPermit`] to access them.
+        let raw_private_key = &private_key.wrapped;
+        Self::from_raw_private_key(raw_private_key, &RawPrivateKeyPermit::new())
+    }
+
     /// Sign the given message and return a digital signature
     fn sign(&self, msg: &[u8]) -> Self::Signature;
 
diff --git a/nearby/crypto/crypto_provider/src/elliptic_curve.rs b/nearby/crypto/crypto_provider/src/elliptic_curve.rs
index d06a21d..d176769 100644
--- a/nearby/crypto/crypto_provider/src/elliptic_curve.rs
+++ b/nearby/crypto/crypto_provider/src/elliptic_curve.rs
@@ -12,12 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-extern crate alloc;
-
 use core::fmt::Debug;
 
-use alloc::vec::Vec;
-
 /// Marker trait for an elliptic curve used for diffie-hellman.
 pub trait Curve {}
 
@@ -42,12 +38,16 @@
     /// The random number generator to be used for generating a secret
     type Rng: crate::CryptoRng;
 
+    /// The for encoded public key bytes, for example a `[u8; N]` array if the size is fixed, or
+    /// `ArrayVec<[u8; N]>` if the size is bounded but not fixed.
+    type EncodedPublicKey: AsRef<[u8]> + Debug;
+
     /// Generates a new random ephemeral secret.
     fn generate_random(rng: &mut Self::Rng) -> Self;
 
     /// Returns the bytes of the public key for this ephemeral secret that is suitable for sending
     /// over the wire for key exchange.
-    fn public_key_bytes(&self) -> Vec<u8>;
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey;
 
     /// Performs diffie-hellman key exchange using this ephemeral secret with the given public key
     /// `other_pub`.
@@ -59,6 +59,8 @@
 
 /// Trait for a public key used for elliptic curve diffie hellman.
 pub trait PublicKey<E: Curve>: Sized + PartialEq + Debug {
+    /// The type for an encoded public key.
+    type EncodedPublicKey: AsRef<[u8]> + Debug;
     /// The error type associated with Public Key.
     type Error: Debug;
 
@@ -71,5 +73,5 @@
     /// the sec1 encoding, may return equivalent but different byte-representations due to point
     /// compression, so it is not necessarily true that `from_bytes(bytes)?.to_bytes() == bytes`
     /// (but it is always true that `from_bytes(key.to_bytes())? == key`).
-    fn to_bytes(&self) -> Vec<u8>;
+    fn to_bytes(&self) -> Self::EncodedPublicKey;
 }
diff --git a/nearby/crypto/crypto_provider/src/lib.rs b/nearby/crypto/crypto_provider/src/lib.rs
index 624072a..0031ebb 100644
--- a/nearby/crypto/crypto_provider/src/lib.rs
+++ b/nearby/crypto/crypto_provider/src/lib.rs
@@ -48,6 +48,8 @@
 /// mod containing traits for ed25519 key generation, signing, and verification
 pub mod ed25519;
 
+pub use tinyvec;
+
 /// Uber crypto trait which defines the traits for all crypto primitives as associated types
 pub trait CryptoProvider: Clone + Debug + PartialEq + Eq + Send {
     /// The Hkdf type which implements the hkdf trait
@@ -80,10 +82,13 @@
     /// using SHA-512 (SHA-2) and Curve25519
     type Ed25519: ed25519::Ed25519Provider;
     /// The trait defining AES-128-GCM-SIV, a nonce-misuse resistant AEAD with a key size of 16 bytes.
-    type Aes128GcmSiv: aead::aes_gcm_siv::AesGcmSiv<Key = Aes128Key>;
+    type Aes128GcmSiv: aead::AesGcmSiv + aead::AeadInit<Aes128Key>;
     /// The trait defining AES-256-GCM-SIV, a nonce-misuse resistant AEAD with a key size of 32 bytes.
-    type Aes256GcmSiv: aead::aes_gcm_siv::AesGcmSiv<Key = Aes256Key>;
-
+    type Aes256GcmSiv: aead::AesGcmSiv + aead::AeadInit<Aes256Key>;
+    /// The trait defining AES-128-GCM, an AEAD with a key size of 16 bytes.
+    type Aes128Gcm: aead::AesGcm + aead::AeadInit<Aes128Key>;
+    /// The trait defining AES-256-GCM, an AEAD with a key size of 32 bytes.
+    type Aes256Gcm: aead::AesGcm + aead::AeadInit<Aes256Key>;
     /// The cryptographically secure random number generator
     type CryptoRng: CryptoRng;
 
diff --git a/nearby/crypto/crypto_provider/src/p256.rs b/nearby/crypto/crypto_provider/src/p256.rs
index 90d3542..fd7b531 100644
--- a/nearby/crypto/crypto_provider/src/p256.rs
+++ b/nearby/crypto/crypto_provider/src/p256.rs
@@ -12,10 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-extern crate alloc;
+use tinyvec::ArrayVec;
 
 use crate::elliptic_curve::{Curve, EcdhProvider, PublicKey};
-use alloc::vec::Vec;
 use core::fmt::Debug;
 
 /// Marker type for P256 implementation. This is used by EcdhProvider as its type parameter.
@@ -23,6 +22,24 @@
 pub enum P256 {}
 impl Curve for P256 {}
 
+/// Longest length for a sec-1 encoded P256 public key, which is the uncompressed format
+/// `04 || X || Y` as defined in section 2.3.3 of the SECG SEC 1 ("Elliptic Curve Cryptography")
+/// standard.
+const P256_PUBLIC_KEY_MAX_LENGTH: usize = 65;
+
+/// Whether an elliptic curve point should be compressed or not.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PointCompression {
+    /// The elliptic curve point should be compressed (`02 || X` or `03 || X`),
+    /// as defined in section 2.3.3 of the SECG SEC 1 ("Elliptic Curve
+    /// Cryptography").
+    Compressed,
+    /// The elliptic curve point should be uncompressed (`04 || X || Y`), as
+    /// defined in section 2.3.3 of the SECG SEC 1 ("Elliptic Curve
+    /// Cryptography").
+    Uncompressed,
+}
+
 /// Trait for a NIST-P256 public key.
 pub trait P256PublicKey: Sized + PartialEq + Debug {
     /// The error type associated with this implementation.
@@ -36,7 +53,10 @@
     /// ("Elliptic Curve Cryptography") standard. Note that it is not necessarily true that
     /// `from_sec1_bytes(bytes)?.to_sec1_bytes() == bytes` because of point compression. (But it is
     /// always true that `from_sec1_bytes(key.to_sec1_bytes())? == key`).
-    fn to_sec1_bytes(&self) -> Vec<u8>;
+    fn to_sec1_bytes(
+        &self,
+        point_compression: PointCompression,
+    ) -> ArrayVec<[u8; P256_PUBLIC_KEY_MAX_LENGTH]>;
 
     /// Converts this public key's x and y coordinates on the elliptic curve to big endian octet
     /// strings.
@@ -48,13 +68,14 @@
 
 impl<P: P256PublicKey> PublicKey<P256> for P {
     type Error = <Self as P256PublicKey>::Error;
+    type EncodedPublicKey = ArrayVec<[u8; P256_PUBLIC_KEY_MAX_LENGTH]>;
 
     fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
         Self::from_sec1_bytes(bytes)
     }
 
-    fn to_bytes(&self) -> Vec<u8> {
-        Self::to_sec1_bytes(self)
+    fn to_bytes(&self) -> Self::EncodedPublicKey {
+        Self::to_sec1_bytes(self, PointCompression::Uncompressed)
     }
 }
 
diff --git a/nearby/crypto/crypto_provider_boringssl/.cargo/config.toml b/nearby/crypto/crypto_provider_boringssl/.cargo/config.toml
deleted file mode 100644
index f5ab7fa..0000000
--- a/nearby/crypto/crypto_provider_boringssl/.cargo/config.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-paths = [
-    "../../../boringssl-build/boringssl/rust/bssl-crypto",
-]
\ No newline at end of file
diff --git a/nearby/crypto/crypto_provider_boringssl/Cargo.lock b/nearby/crypto/crypto_provider_boringssl/Cargo.lock
index 14402c1..7773e56 100644
--- a/nearby/crypto/crypto_provider_boringssl/Cargo.lock
+++ b/nearby/crypto/crypto_provider_boringssl/Cargo.lock
@@ -4,13 +4,20 @@
 
 [[package]]
 name = "base64"
-version = "0.13.1"
+version = "0.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
 
 [[package]]
 name = "bssl-crypto"
 version = "0.1.0"
+dependencies = [
+ "bssl-sys",
+]
+
+[[package]]
+name = "bssl-sys"
+version = "0.1.0"
 
 [[package]]
 name = "cfg-if"
@@ -21,6 +28,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_boringssl"
@@ -28,18 +38,10 @@
 dependencies = [
  "bssl-crypto",
  "crypto_provider",
- "crypto_provider_stubs",
  "crypto_provider_test",
 ]
 
 [[package]]
-name = "crypto_provider_stubs"
-version = "0.1.0"
-dependencies = [
- "crypto_provider",
-]
-
-[[package]]
 name = "crypto_provider_test"
 version = "0.1.0"
 dependencies = [
@@ -56,9 +58,9 @@
 
 [[package]]
 name = "getrandom"
-version = "0.2.9"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
 dependencies = [
  "cfg-if",
  "libc",
@@ -79,24 +81,21 @@
 
 [[package]]
 name = "itoa"
-version = "1.0.6"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
 
 [[package]]
 name = "libc"
-version = "0.2.144"
+version = "0.2.147"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
 
 [[package]]
 name = "log"
-version = "0.4.17"
+version = "0.4.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if",
-]
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
 
 [[package]]
 name = "ppv-lite86"
@@ -106,18 +105,18 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.56"
+version = "1.0.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.27"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
 dependencies = [
  "proc-macro2",
 ]
@@ -197,14 +196,14 @@
 
 [[package]]
 name = "rstest_reuse"
-version = "0.5.0"
+version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45f80dcc84beab3a327bbe161f77db25f336a1452428176787c8c79ac79d7073"
+checksum = "88530b681abe67924d42cca181d070e3ac20e0740569441a9e35a7cedd2b34a4"
 dependencies = [
  "quote",
  "rand",
  "rustc_version",
- "syn 1.0.109",
+ "syn 2.0.29",
 ]
 
 [[package]]
@@ -218,41 +217,41 @@
 
 [[package]]
 name = "ryu"
-version = "1.0.13"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 
 [[package]]
 name = "semver"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
+checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
 
 [[package]]
 name = "serde"
-version = "1.0.162"
+version = "1.0.187"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6"
+checksum = "30a7fe14252655bd1e578af19f5fa00fe02fd0013b100ca6b49fde31c41bae4c"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.162"
+version = "1.0.187"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
+checksum = "e46b2a6ca578b3f1d4501b12f78ed4692006d79d82a1a7c561c12dbc3d625eb8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.15",
+ "syn 2.0.29",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.96"
+version = "1.0.105"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
 dependencies = [
  "itoa",
  "ryu",
@@ -272,9 +271,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.15"
+version = "2.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -290,10 +289,16 @@
 ]
 
 [[package]]
-name = "unicode-ident"
-version = "1.0.8"
+name = "tinyvec"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
 
 [[package]]
 name = "wasi"
@@ -303,9 +308,9 @@
 
 [[package]]
 name = "wycheproof"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "183c789620c674b79dac33cd3aadb6c8006b66cba6a680402235aaebc743e3df"
+checksum = "e639f57253b80c6584b378011aec0fed61c4c21d7a4b97c4d9d7eaf35ca77d12"
 dependencies = [
  "base64",
  "hex",
diff --git a/nearby/crypto/crypto_provider_boringssl/Cargo.toml b/nearby/crypto/crypto_provider_boringssl/Cargo.toml
index 24ce66c..fb3bfdf 100644
--- a/nearby/crypto/crypto_provider_boringssl/Cargo.toml
+++ b/nearby/crypto/crypto_provider_boringssl/Cargo.toml
@@ -6,10 +6,9 @@
 
 [dependencies]
 crypto_provider = { path = "../crypto_provider", features = ["alloc", "std"] }
-crypto_provider_stubs = { path = "../crypto_provider_stubs" }
 
-# Note: before this crate will work you need to run `scripts/prepare-boringssl.sh`
-bssl-crypto = {path = "../bssl-crypto"}
+# Note: before this crate will work you need to run `cargo run -p build_scripts -- build-boringssl`
+bssl-crypto = {path = "../../../third_party/boringssl/rust/bssl-crypto"}
 
 [dev-dependencies]
 crypto_provider_test = {path = "../crypto_provider_test"}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/aead/aes_gcm.rs b/nearby/crypto/crypto_provider_boringssl/src/aead/aes_gcm.rs
new file mode 100644
index 0000000..03bb087
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/aead/aes_gcm.rs
@@ -0,0 +1,89 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate alloc;
+
+use alloc::vec::Vec;
+use bssl_crypto::aead::Aead as _;
+use crypto_provider::aead::{Aead, AeadError, AeadInit};
+use crypto_provider::aes::{Aes128Key, Aes256Key, AesKey};
+
+pub struct AesGcm(bssl_crypto::aead::AesGcm);
+
+impl AeadInit<Aes128Key> for AesGcm {
+    fn new(key: &Aes128Key) -> Self {
+        Self(bssl_crypto::aead::new_aes_128_gcm(key.as_array()))
+    }
+}
+
+impl AeadInit<Aes256Key> for AesGcm {
+    fn new(key: &Aes256Key) -> Self {
+        Self(bssl_crypto::aead::new_aes_256_gcm(key.as_array()))
+    }
+}
+
+impl crypto_provider::aead::AesGcm for AesGcm {}
+
+impl Aead for AesGcm {
+    const TAG_SIZE: usize = 16;
+    type Nonce = [u8; 12];
+    type Tag = [u8; 16];
+
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0.encrypt(msg, aad, nonce).map_err(|_| AeadError)
+    }
+
+    fn encrypt_detached(
+        &self,
+        _msg: &mut [u8],
+        _aad: &[u8],
+        _nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError> {
+        unimplemented!("Not yet supported by boringssl")
+    }
+
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0.decrypt(msg, aad, nonce).map_err(|_| AeadError)
+    }
+
+    fn decrypt_detached(
+        &self,
+        _msg: &mut [u8],
+        _aad: &[u8],
+        _nonce: &Self::Nonce,
+        _tag: &Self::Tag,
+    ) -> Result<(), AeadError> {
+        unimplemented!("Not yet supported by boringssl")
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::marker::PhantomData;
+
+    use crypto_provider_test::aead::aes_gcm::*;
+    use crypto_provider_test::aes::*;
+
+    use super::*;
+
+    #[apply(aes_128_gcm_test_cases)]
+    fn aes_gcm_128_test(testcase: CryptoProviderTestCase<AesGcm>) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_256_gcm_test_cases)]
+    fn aes_gcm_256_test(testcase: CryptoProviderTestCase<AesGcm>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/aead/aes_gcm_siv.rs b/nearby/crypto/crypto_provider_boringssl/src/aead/aes_gcm_siv.rs
new file mode 100644
index 0000000..d1fb0e7
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/aead/aes_gcm_siv.rs
@@ -0,0 +1,88 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+extern crate alloc;
+
+use alloc::vec::Vec;
+use bssl_crypto::aead::Aead as _;
+use crypto_provider::aead::{Aead, AeadError, AeadInit};
+use crypto_provider::aes::{Aes128Key, Aes256Key, AesKey};
+
+pub struct AesGcmSiv(bssl_crypto::aead::AesGcmSiv);
+
+impl AeadInit<Aes128Key> for AesGcmSiv {
+    fn new(key: &Aes128Key) -> Self {
+        Self(bssl_crypto::aead::new_aes_128_gcm_siv(key.as_array()))
+    }
+}
+
+impl AeadInit<Aes256Key> for AesGcmSiv {
+    fn new(key: &Aes256Key) -> Self {
+        Self(bssl_crypto::aead::new_aes_256_gcm_siv(key.as_array()))
+    }
+}
+
+impl crypto_provider::aead::AesGcmSiv for AesGcmSiv {}
+
+impl Aead for AesGcmSiv {
+    const TAG_SIZE: usize = 16;
+    type Nonce = [u8; 12];
+    type Tag = [u8; 16];
+
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0.encrypt(msg, aad, nonce).map_err(|_| AeadError)
+    }
+
+    fn encrypt_detached(
+        &self,
+        _msg: &mut [u8],
+        _aad: &[u8],
+        _nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError> {
+        unimplemented!("Not yet supported by boringssl")
+    }
+
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0.decrypt(msg, aad, nonce).map_err(|_| AeadError)
+    }
+
+    fn decrypt_detached(
+        &self,
+        _msg: &mut [u8],
+        _aad: &[u8],
+        _nonce: &Self::Nonce,
+        _tag: &Self::Tag,
+    ) -> Result<(), AeadError> {
+        unimplemented!("Not yet supported by boringssl")
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::marker::PhantomData;
+
+    use crypto_provider_test::aead::aes_gcm_siv::*;
+    use crypto_provider_test::aes::*;
+
+    use super::*;
+
+    #[apply(aes_128_gcm_siv_test_cases)]
+    fn aes_gcm_siv_128_test(testcase: CryptoProviderTestCase<AesGcmSiv>) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_256_gcm_siv_test_cases)]
+    fn aes_gcm_siv_256_test(testcase: CryptoProviderTestCase<AesGcmSiv>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/bssl-crypto/src/lib.rs b/nearby/crypto/crypto_provider_boringssl/src/aead/mod.rs
similarity index 73%
copy from nearby/crypto/bssl-crypto/src/lib.rs
copy to nearby/crypto/crypto_provider_boringssl/src/aead/mod.rs
index 89e6968..424f16e 100644
--- a/nearby/crypto/bssl-crypto/src/lib.rs
+++ b/nearby/crypto/crypto_provider_boringssl/src/aead/mod.rs
@@ -4,7 +4,7 @@
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
 //
-//     http://www.apache.org/licenses/LICENSE-2.0
+//      http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
@@ -12,5 +12,5 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Placeholder crate to satisfy cargo. If actually using boring ssl, please run the
-//! `build-boringssl` subcommand of the top level crate.
+pub(crate) mod aes_gcm;
+pub(crate) mod aes_gcm_siv;
diff --git a/nearby/crypto/crypto_provider_boringssl/src/aes/cbc.rs b/nearby/crypto/crypto_provider_boringssl/src/aes/cbc.rs
new file mode 100644
index 0000000..4d3fd3f
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/aes/cbc.rs
@@ -0,0 +1,81 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate alloc;
+
+use alloc::vec::Vec;
+use bssl_crypto::cipher::BlockCipher;
+use crypto_provider::{
+    aes::{
+        cbc::{AesCbcIv, DecryptionError, EncryptionError},
+        Aes256Key, AesKey,
+    },
+    tinyvec::SliceVec,
+};
+
+/// BoringSSL implementation of AES-CBC with PKCS7 padding
+pub enum AesCbcPkcs7Padded {}
+impl crypto_provider::aes::cbc::AesCbcPkcs7Padded for AesCbcPkcs7Padded {
+    #[allow(clippy::expect_used)]
+    fn encrypt(key: &Aes256Key, iv: &AesCbcIv, message: &[u8]) -> Vec<u8> {
+        let encryptor = bssl_crypto::cipher::aes_cbc::Aes256Cbc::new_encrypt(key.as_array(), iv);
+        encryptor.encrypt_padded(message).expect("Encrypting AES-CBC should be infallible")
+    }
+
+    fn encrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        message: &mut SliceVec<u8>,
+    ) -> Result<(), EncryptionError> {
+        let result = Self::encrypt(key, iv, message);
+        if message.capacity() < result.len() {
+            return Err(EncryptionError::PaddingFailed);
+        }
+        message.clear();
+        message.extend_from_slice(&result);
+        Ok(())
+    }
+
+    fn decrypt(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &[u8],
+    ) -> Result<Vec<u8>, DecryptionError> {
+        let decryptor = bssl_crypto::cipher::aes_cbc::Aes256Cbc::new_decrypt(key.as_array(), iv);
+        decryptor.decrypt_padded(ciphertext).map_err(|_| DecryptionError::BadPadding)
+    }
+
+    fn decrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &mut SliceVec<u8>,
+    ) -> Result<(), DecryptionError> {
+        Self::decrypt(key, iv, ciphertext).map(|result| {
+            ciphertext.clear();
+            ciphertext.extend_from_slice(&result);
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::AesCbcPkcs7Padded;
+    use core::marker::PhantomData;
+    use crypto_provider_test::aes::cbc::*;
+
+    #[apply(aes_256_cbc_test_cases)]
+    fn aes_256_cbc_test(testcase: CryptoProviderTestCase<AesCbcPkcs7Padded>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/aes/ctr.rs b/nearby/crypto/crypto_provider_boringssl/src/aes/ctr.rs
new file mode 100644
index 0000000..c13ab31
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/aes/ctr.rs
@@ -0,0 +1,74 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use bssl_crypto::cipher::StreamCipher;
+use crypto_provider::aes::{ctr::NonceAndCounter, Aes128Key, Aes256Key, AesKey};
+
+/// BoringSSL implementation of AES-CTR 128.
+pub struct AesCtr128(bssl_crypto::cipher::aes_ctr::Aes128Ctr);
+
+impl crypto_provider::aes::ctr::AesCtr for AesCtr128 {
+    type Key = Aes128Key;
+
+    fn new(key: &Self::Key, nonce_and_counter: NonceAndCounter) -> Self {
+        Self(bssl_crypto::cipher::aes_ctr::Aes128Ctr::new(
+            key.as_array(),
+            &nonce_and_counter.as_block_array(),
+        ))
+    }
+
+    #[allow(clippy::expect_used)]
+    fn apply_keystream(&mut self, data: &mut [u8]) {
+        assert!(data.len() < i32::MAX as usize);
+        self.0.apply_keystream(data).expect("Data length should fit inside of a i32")
+    }
+}
+
+/// BoringSSL implementation of AES-CTR 256.
+pub struct AesCtr256(bssl_crypto::cipher::aes_ctr::Aes256Ctr);
+
+impl crypto_provider::aes::ctr::AesCtr for AesCtr256 {
+    type Key = Aes256Key;
+
+    fn new(key: &Self::Key, nonce_and_counter: NonceAndCounter) -> Self {
+        Self(bssl_crypto::cipher::aes_ctr::Aes256Ctr::new(
+            key.as_array(),
+            &nonce_and_counter.as_block_array(),
+        ))
+    }
+
+    #[allow(clippy::expect_used)]
+    fn apply_keystream(&mut self, data: &mut [u8]) {
+        assert!(data.len() < i32::MAX as usize);
+        self.0.apply_keystream(data).expect("Data length should fit inside of a i32")
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use core::marker::PhantomData;
+    use crypto_provider_test::aes::ctr::*;
+    use crypto_provider_test::aes::*;
+
+    #[apply(aes_128_ctr_test_cases)]
+    fn aes_128_ctr_test(testcase: CryptoProviderTestCase<AesCtr128>) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_256_ctr_test_cases)]
+    fn aes_256_ctr_test(testcase: CryptoProviderTestCase<AesCtr256>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/aes.rs b/nearby/crypto/crypto_provider_boringssl/src/aes/mod.rs
similarity index 97%
rename from nearby/crypto/crypto_provider_boringssl/src/aes.rs
rename to nearby/crypto/crypto_provider_boringssl/src/aes/mod.rs
index 88bfeac..d5cbe58 100644
--- a/nearby/crypto/crypto_provider_boringssl/src/aes.rs
+++ b/nearby/crypto/crypto_provider_boringssl/src/aes/mod.rs
@@ -17,6 +17,12 @@
     Aes, Aes128Key, Aes256Key, AesBlock, AesCipher, AesDecryptCipher, AesEncryptCipher, AesKey,
 };
 
+/// AES_CTR implementations.
+pub mod ctr;
+
+/// AES_CBC implementations.
+pub mod cbc;
+
 /// BoringSSL AES-128 operations
 pub struct Aes128;
 impl Aes for Aes128 {
diff --git a/nearby/crypto/crypto_provider_boringssl/src/ed25519.rs b/nearby/crypto/crypto_provider_boringssl/src/ed25519.rs
index 7c671ae..b5c374a 100644
--- a/nearby/crypto/crypto_provider_boringssl/src/ed25519.rs
+++ b/nearby/crypto/crypto_provider_boringssl/src/ed25519.rs
@@ -13,7 +13,8 @@
 // limitations under the License.
 
 use crypto_provider::ed25519::{
-    InvalidBytes, RawPrivateKey, RawPublicKey, RawSignature, Signature as _, SignatureError,
+    InvalidBytes, RawPrivateKey, RawPrivateKeyPermit, RawPublicKey, RawSignature, Signature as _,
+    SignatureError,
 };
 
 pub struct Ed25519;
@@ -30,11 +31,11 @@
     type PublicKey = PublicKey;
     type Signature = Signature;
 
-    fn private_key(&self) -> RawPrivateKey {
+    fn raw_private_key(&self, _permit: &RawPrivateKeyPermit) -> RawPrivateKey {
         self.0.to_seed()
     }
 
-    fn from_private_key(bytes: &RawPrivateKey) -> Self
+    fn from_raw_private_key(bytes: &RawPrivateKey, _permit: &RawPrivateKeyPermit) -> Self
     where
         Self: Sized,
     {
diff --git a/nearby/crypto/crypto_provider_boringssl/src/lib.rs b/nearby/crypto/crypto_provider_boringssl/src/lib.rs
index 3b78898..75758b6 100644
--- a/nearby/crypto/crypto_provider_boringssl/src/lib.rs
+++ b/nearby/crypto/crypto_provider_boringssl/src/lib.rs
@@ -21,14 +21,12 @@
     clippy::expect_used
 )]
 
-//! Crate which provides impls for CryptoProvider backed by BoringSSL.
-
-use bssl_crypto::digest::{Sha256, Sha512};
+//! Crate which provides impls for CryptoProvider backed by BoringSSL
+//!
 use bssl_crypto::rand::rand_bytes;
 use crypto_provider::{CryptoProvider, CryptoRng};
-use crypto_provider_stubs::*;
 
-/// Implementation of `crypto_provider::aes` types using BoringSSL.
+/// Implementation of `crypto_provider::aes` types using BoringSSL
 pub mod aes;
 
 /// Implementations of crypto_provider::hkdf traits backed by BoringSSL
@@ -40,31 +38,45 @@
 /// Implementations of crypto_provider::ed25519 traits backed by BoringSSL
 mod ed25519;
 
+/// Implementations of crypto_provider::aead traits backed by BoringSSL
+mod aead;
+
+/// Implementations of crypto_provider::p256 traits backed by BoringSSL
+mod p256;
+
+/// Implementations of crypto_provider::x25519 traits backed by BoringSSL
+mod x25519;
+
+/// Implementations of crypto_provider::sha2 traits backed by BoringSSL
+mod sha2;
+
 /// The BoringSSL backed struct which implements CryptoProvider
 #[derive(Default, Clone, Debug, PartialEq, Eq)]
 pub struct Boringssl;
 
 impl CryptoProvider for Boringssl {
-    type HkdfSha256 = hkdf::Hkdf<Sha256>;
+    type HkdfSha256 = hkdf::Hkdf<bssl_crypto::digest::Sha256>;
     type HmacSha256 = hmac::HmacSha256;
-    type HkdfSha512 = hkdf::Hkdf<Sha512>;
+    type HkdfSha512 = hkdf::Hkdf<bssl_crypto::digest::Sha512>;
     type HmacSha512 = hmac::HmacSha512;
-    type AesCbcPkcs7Padded = AesCbcPkcs7PaddedStubs;
-    type X25519 = X25519Stubs;
-    type P256 = P256Stubs;
-    type Sha256 = Sha2Stubs;
-    type Sha512 = Sha2Stubs;
+    type AesCbcPkcs7Padded = aes::cbc::AesCbcPkcs7Padded;
+    type X25519 = x25519::X25519Ecdh;
+    type P256 = p256::P256Ecdh;
+    type Sha256 = sha2::Sha256;
+    type Sha512 = sha2::Sha512;
     type Aes128 = aes::Aes128;
     type Aes256 = aes::Aes256;
-    type AesCtr128 = Aes128Stubs;
-    type AesCtr256 = Aes256Stubs;
+    type AesCtr128 = aes::ctr::AesCtr128;
+    type AesCtr256 = aes::ctr::AesCtr256;
     type Ed25519 = ed25519::Ed25519;
-    type Aes128GcmSiv = Aes128Stubs;
-    type Aes256GcmSiv = Aes256Stubs;
+    type Aes128GcmSiv = aead::aes_gcm_siv::AesGcmSiv;
+    type Aes256GcmSiv = aead::aes_gcm_siv::AesGcmSiv;
+    type Aes128Gcm = aead::aes_gcm::AesGcm;
+    type Aes256Gcm = aead::aes_gcm::AesGcm;
     type CryptoRng = BoringSslRng;
 
-    fn constant_time_eq(_a: &[u8], _b: &[u8]) -> bool {
-        unimplemented!()
+    fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
+        bssl_crypto::mem::crypto_memcmp(a, b)
     }
 }
 
@@ -86,3 +98,17 @@
         rand_bytes(dest)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use core::marker::PhantomData;
+    use crypto_provider_test::prelude::*;
+    use crypto_provider_test::sha2::*;
+
+    use crate::Boringssl;
+
+    #[apply(sha2_test_cases)]
+    fn sha2_tests(testcase: CryptoProviderTestCase<Boringssl>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/p256.rs b/nearby/crypto/crypto_provider_boringssl/src/p256.rs
new file mode 100644
index 0000000..7fefecd
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/p256.rs
@@ -0,0 +1,117 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate alloc;
+
+use bssl_crypto::ecdh::{PrivateKey, PublicKey};
+use crypto_provider::{
+    elliptic_curve::{EcdhProvider, EphemeralSecret, PublicKey as _},
+    p256::{PointCompression, P256},
+    tinyvec::ArrayVec,
+};
+
+/// Implementation of NIST-P256 using bssl-crypto crate.
+pub struct P256Ecdh;
+
+impl EcdhProvider<P256> for P256Ecdh {
+    type PublicKey = P256PublicKey;
+    type EphemeralSecret = P256EphemeralSecret;
+    type SharedSecret = [u8; 32];
+}
+
+/// A NIST-P256 public key.
+#[derive(Debug, PartialEq, Eq)]
+pub struct P256PublicKey(PublicKey<bssl_crypto::ecdh::P256>);
+impl crypto_provider::p256::P256PublicKey for P256PublicKey {
+    type Error = bssl_crypto::ecdh::Error;
+
+    fn from_sec1_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
+        PublicKey::try_from(bytes).map(Self)
+    }
+
+    fn to_sec1_bytes(&self, point_compression: PointCompression) -> ArrayVec<[u8; 65]> {
+        match point_compression {
+            PointCompression::Compressed => unimplemented!(),
+            PointCompression::Uncompressed => {
+                let mut bytes = ArrayVec::<[u8; 65]>::new();
+                bytes.extend_from_slice(&self.0.to_vec());
+                bytes
+            }
+        }
+    }
+
+    #[allow(clippy::expect_used)]
+    fn to_affine_coordinates(&self) -> Result<([u8; 32], [u8; 32]), Self::Error> {
+        Ok(self.0.to_affine_coordinates())
+    }
+    fn from_affine_coordinates(x: &[u8; 32], y: &[u8; 32]) -> Result<Self, Self::Error> {
+        PublicKey::<bssl_crypto::ecdh::P256>::from_affine_coordinates(x, y).map(Self)
+    }
+}
+
+/// Ephemeral secrect for use in a P256 Diffie-Hellman
+pub struct P256EphemeralSecret {
+    secret: PrivateKey<bssl_crypto::ecdh::P256>,
+}
+
+impl EphemeralSecret<P256> for P256EphemeralSecret {
+    type Impl = P256Ecdh;
+    type Error = bssl_crypto::ecdh::Error;
+    type Rng = ();
+    type EncodedPublicKey =
+        <P256PublicKey as crypto_provider::elliptic_curve::PublicKey<P256>>::EncodedPublicKey;
+
+    fn generate_random(_rng: &mut Self::Rng) -> Self {
+        Self { secret: PrivateKey::generate() }
+    }
+
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
+        P256PublicKey((&self.secret).into()).to_bytes()
+    }
+
+    fn diffie_hellman(
+        self,
+        other_pub: &P256PublicKey,
+    ) -> Result<<Self::Impl as EcdhProvider<P256>>::SharedSecret, Self::Error> {
+        let shared_secret = self.secret.diffie_hellman(&other_pub.0)?;
+        let bytes: <Self::Impl as EcdhProvider<P256>>::SharedSecret = shared_secret.to_bytes();
+        Ok(bytes)
+    }
+}
+
+#[cfg(test)]
+impl crypto_provider_test::elliptic_curve::EphemeralSecretForTesting<P256> for P256EphemeralSecret {
+    #[allow(clippy::unwrap_used)]
+    fn from_private_components(
+        private_bytes: &[u8; 32],
+        _public_key: &P256PublicKey,
+    ) -> Result<Self, Self::Error> {
+        Ok(Self { secret: PrivateKey::from_private_bytes(private_bytes).unwrap() })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::P256Ecdh;
+    use core::marker::PhantomData;
+    use crypto_provider_test::p256::*;
+
+    #[apply(p256_test_cases)]
+    fn p256_tests(testcase: CryptoProviderTestCase<P256Ecdh>, name: &str) {
+        if name == "to_bytes_compressed" {
+            return; // EC point compression not supported by bssl-crypto yet
+        }
+        testcase(PhantomData::<P256Ecdh>)
+    }
+}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/sha2.rs b/nearby/crypto/crypto_provider_boringssl/src/sha2.rs
new file mode 100644
index 0000000..d95294d
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/sha2.rs
@@ -0,0 +1,35 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/// BoringSSL implementation for SHA256
+pub struct Sha256;
+
+impl crypto_provider::sha2::Sha256 for Sha256 {
+    fn sha256(input: &[u8]) -> [u8; 32] {
+        let mut digest = bssl_crypto::digest::Sha256::new_digest();
+        digest.update(input);
+        digest.finalize()
+    }
+}
+
+/// BoringSSL implementation for SHA512
+pub struct Sha512;
+
+impl crypto_provider::sha2::Sha512 for Sha512 {
+    fn sha512(input: &[u8]) -> [u8; 64] {
+        let mut digest = bssl_crypto::digest::Sha512::new_digest();
+        digest.update(input);
+        digest.finalize()
+    }
+}
diff --git a/nearby/crypto/crypto_provider_boringssl/src/x25519.rs b/nearby/crypto/crypto_provider_boringssl/src/x25519.rs
new file mode 100644
index 0000000..58d040b
--- /dev/null
+++ b/nearby/crypto/crypto_provider_boringssl/src/x25519.rs
@@ -0,0 +1,102 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crypto_provider::elliptic_curve::{EcdhProvider, EphemeralSecret, PublicKey};
+use crypto_provider::x25519::X25519;
+
+/// The BoringSSL implementation of X25519 ECDH.
+pub struct X25519Ecdh;
+
+impl EcdhProvider<X25519> for X25519Ecdh {
+    type PublicKey = X25519PublicKey;
+    type EphemeralSecret = X25519EphemeralSecret;
+    type SharedSecret = [u8; 32];
+}
+
+/// A X25519 ephemeral secret used for Diffie-Hellman.
+pub struct X25519EphemeralSecret {
+    secret: bssl_crypto::x25519::PrivateKey,
+}
+
+impl EphemeralSecret<X25519> for X25519EphemeralSecret {
+    type Impl = X25519Ecdh;
+    type Error = bssl_crypto::x25519::DiffieHellmanError;
+    type Rng = ();
+    type EncodedPublicKey = [u8; 32];
+
+    fn generate_random(_rng: &mut Self::Rng) -> Self {
+        Self { secret: bssl_crypto::x25519::PrivateKey::generate() }
+    }
+
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
+        let pubkey: bssl_crypto::x25519::PublicKey = (&self.secret).into();
+        pubkey.to_bytes()
+    }
+
+    fn diffie_hellman(
+        self,
+        other_pub: &<X25519Ecdh as EcdhProvider<X25519>>::PublicKey,
+    ) -> Result<<X25519Ecdh as EcdhProvider<X25519>>::SharedSecret, Self::Error> {
+        self.secret.diffie_hellman(&other_pub.0).map(|secret| secret.to_bytes())
+    }
+}
+
+#[cfg(test)]
+impl crypto_provider_test::elliptic_curve::EphemeralSecretForTesting<X25519>
+    for X25519EphemeralSecret
+{
+    fn from_private_components(
+        private_bytes: &[u8; 32],
+        _public_key: &X25519PublicKey,
+    ) -> Result<Self, Self::Error> {
+        Ok(Self { secret: bssl_crypto::x25519::PrivateKey::from_private_bytes(private_bytes) })
+    }
+}
+
+/// A X25519 public key.
+#[derive(Debug, PartialEq, Eq)]
+pub struct X25519PublicKey(bssl_crypto::x25519::PublicKey);
+
+impl PublicKey<X25519> for X25519PublicKey {
+    type Error = Error;
+    type EncodedPublicKey = [u8; 32];
+
+    fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
+        let byte_arr: [u8; 32] = bytes.try_into().map_err(|_| Error::WrongSize)?;
+        Ok(Self(bssl_crypto::x25519::PublicKey::from(&byte_arr)))
+    }
+
+    fn to_bytes(&self) -> Self::EncodedPublicKey {
+        self.0.to_bytes()
+    }
+}
+
+/// Error type for the BoringSSL implementation of x25519.
+#[derive(Debug)]
+pub enum Error {
+    /// Unexpected size for the given input.
+    WrongSize,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::X25519Ecdh;
+    use core::marker::PhantomData;
+    use crypto_provider_test::x25519::*;
+
+    #[apply(x25519_test_cases)]
+    fn x25519_tests(testcase: CryptoProviderTestCase<X25519Ecdh>) {
+        testcase(PhantomData::<X25519Ecdh>)
+    }
+}
diff --git a/nearby/crypto/crypto_provider_default/src/lib.rs b/nearby/crypto/crypto_provider_default/src/lib.rs
index 4b1b01e..b880ba8 100644
--- a/nearby/crypto/crypto_provider_default/src/lib.rs
+++ b/nearby/crypto/crypto_provider_default/src/lib.rs
@@ -15,6 +15,10 @@
 //! Provides multiple implementations of CryptoProvider through the same struct, configurable by
 //! feature flag.
 
+#![no_std]
+#![forbid(unsafe_code)]
+#![deny(missing_docs)]
+
 cfg_if::cfg_if! {
     if #[cfg(feature = "rustcrypto")] {
         pub use crypto_provider_rustcrypto::RustCrypto as CryptoProviderImpl;
diff --git a/nearby/crypto/crypto_provider_openssl/src/aes.rs b/nearby/crypto/crypto_provider_openssl/src/aes.rs
index f5d5b00..c2ac5b1 100644
--- a/nearby/crypto/crypto_provider_openssl/src/aes.rs
+++ b/nearby/crypto/crypto_provider_openssl/src/aes.rs
@@ -26,10 +26,13 @@
 
 use openssl::symm::{Cipher, Crypter, Mode};
 
-use crypto_provider::aes::{
-    cbc::{AesCbcIv, DecryptionError},
-    ctr::NonceAndCounter,
-    Aes, Aes128Key, Aes256Key, AesBlock, AesCipher, AesDecryptCipher, AesEncryptCipher, AesKey,
+use crypto_provider::{
+    aes::{
+        cbc::{AesCbcIv, DecryptionError, EncryptionError},
+        ctr::NonceAndCounter,
+        Aes, Aes128Key, Aes256Key, AesBlock, AesCipher, AesDecryptCipher, AesEncryptCipher, AesKey,
+    },
+    tinyvec::SliceVec,
 };
 
 /// Uber struct which contains impls for AES-128 fns
@@ -126,7 +129,23 @@
 impl crypto_provider::aes::cbc::AesCbcPkcs7Padded for OpenSslAesCbcPkcs7 {
     fn encrypt(key: &crypto_provider::aes::Aes256Key, iv: &AesCbcIv, message: &[u8]) -> Vec<u8> {
         openssl::symm::encrypt(Cipher::aes_256_cbc(), key.as_slice(), Some(iv.as_slice()), message)
-            .unwrap()
+            // The output buffer is allocated by the openssl crate and guarantees to have enough
+            // space to hold the output value and does not overlap with the input slice.
+            .expect("encrypt should always succeed")
+    }
+
+    fn encrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        message: &mut SliceVec<u8>,
+    ) -> Result<(), EncryptionError> {
+        let encrypted = Self::encrypt(key, iv, message);
+        if encrypted.len() > message.capacity() {
+            return Err(EncryptionError::PaddingFailed);
+        }
+        message.clear();
+        message.extend_from_slice(&encrypted);
+        Ok(())
     }
 
     fn decrypt(
@@ -142,12 +161,22 @@
         )
         .map_err(|_| DecryptionError::BadPadding)
     }
+
+    fn decrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &mut SliceVec<u8>,
+    ) -> Result<(), DecryptionError> {
+        Self::decrypt(key, iv, ciphertext).map(|result| {
+            ciphertext.clear();
+            ciphertext.extend_from_slice(&result);
+        })
+    }
 }
 
 /// OpenSSL implementation of AES-CTR-128
 pub struct OpenSslAesCtr128 {
     enc_cipher: Crypter,
-    dec_cipher: Crypter,
 }
 
 impl crypto_provider::aes::ctr::AesCtr for OpenSslAesCtr128 {
@@ -161,33 +190,19 @@
                 Some(&nonce_and_counter.as_block_array()),
             )
             .unwrap(),
-            dec_cipher: Crypter::new(
-                Cipher::aes_128_ctr(),
-                Mode::Decrypt,
-                key.as_slice(),
-                Some(&nonce_and_counter.as_block_array()),
-            )
-            .unwrap(),
         }
     }
 
-    fn encrypt(&mut self, data: &mut [u8]) {
+    fn apply_keystream(&mut self, data: &mut [u8]) {
         let mut in_slice = vec![0u8; data.len()];
         in_slice.copy_from_slice(data);
         let _ = self.enc_cipher.update(&in_slice, data);
     }
-
-    fn decrypt(&mut self, data: &mut [u8]) {
-        let mut in_slice = vec![0u8; data.len()];
-        in_slice.copy_from_slice(data);
-        let _ = self.dec_cipher.update(&in_slice, data);
-    }
 }
 
 /// OpenSSL implementation of AES-CTR-256
 pub struct OpenSslAesCtr256 {
     enc_cipher: Crypter,
-    dec_cipher: Crypter,
 }
 
 impl crypto_provider::aes::ctr::AesCtr for OpenSslAesCtr256 {
@@ -201,27 +216,14 @@
                 Some(&nonce_and_counter.as_block_array()),
             )
             .unwrap(),
-            dec_cipher: Crypter::new(
-                Cipher::aes_256_ctr(),
-                Mode::Decrypt,
-                key.as_slice(),
-                Some(&nonce_and_counter.as_block_array()),
-            )
-            .unwrap(),
         }
     }
 
-    fn encrypt(&mut self, data: &mut [u8]) {
+    fn apply_keystream(&mut self, data: &mut [u8]) {
         let mut in_slice = vec![0u8; data.len()];
         in_slice.copy_from_slice(data);
         let _ = self.enc_cipher.update(&in_slice, data);
     }
-
-    fn decrypt(&mut self, data: &mut [u8]) {
-        let mut in_slice = vec![0u8; data.len()];
-        in_slice.copy_from_slice(data);
-        let _ = self.dec_cipher.update(&in_slice, data);
-    }
 }
 
 #[cfg(test)]
diff --git a/nearby/crypto/crypto_provider_openssl/src/ed25519.rs b/nearby/crypto/crypto_provider_openssl/src/ed25519.rs
index 110daf2..111348a 100644
--- a/nearby/crypto/crypto_provider_openssl/src/ed25519.rs
+++ b/nearby/crypto/crypto_provider_openssl/src/ed25519.rs
@@ -13,7 +13,8 @@
 // limitations under the License.
 
 use crypto_provider::ed25519::{
-    InvalidBytes, RawPrivateKey, RawPublicKey, RawSignature, Signature as _, SignatureError,
+    InvalidBytes, RawPrivateKey, RawPrivateKeyPermit, RawPublicKey, RawSignature, Signature as _,
+    SignatureError,
 };
 use openssl::pkey::{Id, PKey, Private};
 use openssl::sign::{Signer, Verifier};
@@ -32,7 +33,7 @@
     type PublicKey = PublicKey;
     type Signature = Signature;
 
-    fn private_key(&self) -> RawPrivateKey {
+    fn raw_private_key(&self, _permit: &RawPrivateKeyPermit) -> RawPrivateKey {
         let private_key = self.0.raw_private_key().unwrap();
         let mut public_key = self.0.raw_public_key().unwrap();
         let mut result = private_key;
@@ -40,7 +41,7 @@
         result.try_into().unwrap()
     }
 
-    fn from_private_key(bytes: &RawPrivateKey) -> Self {
+    fn from_raw_private_key(bytes: &RawPrivateKey, _permit: &RawPrivateKeyPermit) -> Self {
         Self(PKey::private_key_from_raw_bytes(bytes, Id::ED25519).unwrap())
     }
 
diff --git a/nearby/crypto/crypto_provider_openssl/src/lib.rs b/nearby/crypto/crypto_provider_openssl/src/lib.rs
index d8157d2..8d1db1d 100644
--- a/nearby/crypto/crypto_provider_openssl/src/lib.rs
+++ b/nearby/crypto/crypto_provider_openssl/src/lib.rs
@@ -82,6 +82,8 @@
     type Ed25519 = ed25519::Ed25519;
     type Aes128GcmSiv = crypto_provider_stubs::Aes128Stubs;
     type Aes256GcmSiv = crypto_provider_stubs::Aes256Stubs;
+    type Aes128Gcm = crypto_provider_stubs::Aes128Stubs;
+    type Aes256Gcm = crypto_provider_stubs::Aes256Stubs;
     type CryptoRng = OpenSslRng;
 
     fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
@@ -113,7 +115,7 @@
     use core::marker::PhantomData;
 
     use crypto_provider_test::sha2::*;
-    use crypto_provider_test::*;
+    use crypto_provider_test::{prelude::*, *};
 
     use crate::Openssl;
 
diff --git a/nearby/crypto/crypto_provider_openssl/src/p256.rs b/nearby/crypto/crypto_provider_openssl/src/p256.rs
index 2f5b0b1..17dd6ce 100644
--- a/nearby/crypto/crypto_provider_openssl/src/p256.rs
+++ b/nearby/crypto/crypto_provider_openssl/src/p256.rs
@@ -12,8 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crypto_provider::elliptic_curve::{EcdhProvider, EphemeralSecret};
-use crypto_provider::p256::P256;
+use crypto_provider::{
+    elliptic_curve::{EcdhProvider, EphemeralSecret},
+    p256::{PointCompression, P256},
+    tinyvec::ArrayVec,
+};
 use openssl::bn::{BigNum, BigNumContext};
 use openssl::derive::Deriver;
 use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm};
@@ -65,15 +68,24 @@
         Ok(Self(eckey.try_into()?))
     }
 
-    fn to_sec1_bytes(&self) -> Vec<u8> {
+    fn to_sec1_bytes(&self, point_compression: PointCompression) -> ArrayVec<[u8; 65]> {
+        let point_conversion_form = match point_compression {
+            PointCompression::Compressed => PointConversionForm::COMPRESSED,
+            PointCompression::Uncompressed => PointConversionForm::UNCOMPRESSED,
+        };
         let ecgroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
         let mut bncontext = BigNumContext::new().unwrap();
-        self.0
-            .ec_key()
-            .unwrap()
-            .public_key()
-            .to_bytes(&ecgroup, PointConversionForm::COMPRESSED, &mut bncontext)
-            .unwrap()
+        let mut bytes = ArrayVec::<[u8; 65]>::new();
+        bytes.extend_from_slice(
+            &self
+                .0
+                .ec_key()
+                .unwrap()
+                .public_key()
+                .to_bytes(&ecgroup, point_conversion_form, &mut bncontext)
+                .unwrap(),
+        );
+        bytes
     }
 
     fn from_affine_coordinates(x: &[u8; 32], y: &[u8; 32]) -> Result<Self, Self::Error> {
@@ -115,6 +127,7 @@
     type Impl = P256Ecdh;
     type Error = Error;
     type Rng = ();
+    type EncodedPublicKey = ArrayVec<[u8; 65]>;
 
     fn generate_random(_rng: &mut Self::Rng) -> Self {
         let ecgroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
@@ -122,15 +135,20 @@
         Self(eckey.try_into().unwrap())
     }
 
-    fn public_key_bytes(&self) -> Vec<u8> {
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
         let ecgroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
         let mut bncontext = BigNumContext::new().unwrap();
-        self.0
-            .ec_key()
-            .unwrap()
-            .public_key()
-            .to_bytes(&ecgroup, PointConversionForm::COMPRESSED, &mut bncontext)
-            .unwrap()
+        let mut bytes = Self::EncodedPublicKey::new();
+        bytes.extend_from_slice(
+            &self
+                .0
+                .ec_key()
+                .unwrap()
+                .public_key()
+                .to_bytes(&ecgroup, PointConversionForm::COMPRESSED, &mut bncontext)
+                .unwrap(),
+        );
+        bytes
     }
 
     fn diffie_hellman(
@@ -178,7 +196,7 @@
     use crypto_provider_test::p256::*;
 
     #[apply(p256_test_cases)]
-    fn p256_tests(testcase: CryptoProviderTestCase<P256Ecdh>) {
-        testcase(PhantomData::<P256Ecdh>)
+    fn p256_tests(testcase: CryptoProviderTestCase<P256Ecdh>, _name: &str) {
+        testcase(PhantomData)
     }
 }
diff --git a/nearby/crypto/crypto_provider_openssl/src/x25519.rs b/nearby/crypto/crypto_provider_openssl/src/x25519.rs
index ff6f3b0..2cb4cab 100644
--- a/nearby/crypto/crypto_provider_openssl/src/x25519.rs
+++ b/nearby/crypto/crypto_provider_openssl/src/x25519.rs
@@ -30,14 +30,15 @@
 
 impl PublicKey<X25519> for X25519PublicKey {
     type Error = ErrorStack;
+    type EncodedPublicKey = [u8; 32];
 
     fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
         let key = PKey::public_key_from_raw_bytes(bytes, Id::X25519)?;
         Ok(X25519PublicKey(key))
     }
 
-    fn to_bytes(&self) -> Vec<u8> {
-        self.0.raw_public_key().unwrap()
+    fn to_bytes(&self) -> Self::EncodedPublicKey {
+        self.0.raw_public_key().unwrap().try_into().unwrap()
     }
 }
 
@@ -48,14 +49,15 @@
     type Impl = X25519Ecdh;
     type Error = ErrorStack;
     type Rng = ();
+    type EncodedPublicKey = [u8; 32];
 
     fn generate_random(_rng: &mut Self::Rng) -> Self {
         let private_key = openssl::pkey::PKey::generate_x25519().unwrap();
         Self(private_key)
     }
 
-    fn public_key_bytes(&self) -> Vec<u8> {
-        self.0.raw_public_key().unwrap()
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
+        self.0.raw_public_key().unwrap().try_into().unwrap()
     }
 
     fn diffie_hellman(
diff --git a/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml b/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml
index e0bf534..a0638ff 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml
+++ b/nearby/crypto/crypto_provider_rustcrypto/Cargo.toml
@@ -6,15 +6,22 @@
 
 [dependencies]
 aead = "0.5.1"
-aes-gcm-siv = { version = "0.11.1", features = ["aes"] }
-crypto_provider.workspace = true
+aes-gcm-siv = { version = "0.11.1", features = [
+    "aes",
+], default-features = false }
+aes-gcm = { version = "0.10.3", features = [
+    "aes",
+], default-features = false }
+crypto_provider = { workspace = true }
 hmac.workspace = true
 hkdf.workspace = true
 sha2.workspace = true
 x25519-dalek.workspace = true
 p256 = { workspace = true, features = ["ecdh"], default-features = false }
 sec1.workspace = true
-ed25519-dalek = { workspace = true, default-features = false, features = ["rand_core"] }
+ed25519-dalek = { workspace = true, default-features = false, features = [
+    "rand_core",
+] }
 rand = { workspace = true, default-features = false }
 rand_core.workspace = true
 subtle.workspace = true
@@ -31,5 +38,12 @@
 
 [features]
 default = ["alloc", "rand_chacha"]
-std = ["ed25519-dalek/default", "rand/std", "rand/std_rng", "crypto_provider/std", "crypto_provider/alloc"]
-alloc = ["aead/bytes"]
+std = [
+    "alloc",
+    "ed25519-dalek/default",
+    "rand/std",
+    "rand/std_rng",
+    "crypto_provider/std",
+    "crypto_provider/alloc",
+]
+alloc = ["aead/bytes", "aead/alloc", "cbc/alloc", "crypto_provider/alloc"]
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm.rs
new file mode 100644
index 0000000..f1d9e0f
--- /dev/null
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm.rs
@@ -0,0 +1,123 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+#[cfg(feature = "alloc")]
+use aead::Payload;
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+
+// RustCrypto defined traits and types
+use aes::cipher::typenum::consts::{U12, U16};
+use aes::cipher::BlockCipher;
+use aes::cipher::BlockEncrypt;
+#[cfg(feature = "alloc")]
+use aes_gcm::aead::Aead as _;
+use aes_gcm::aead::KeyInit;
+use aes_gcm::AeadInPlace as _;
+
+// CryptoProvider traits and types
+use crypto_provider::aead::{Aead, AeadError, AeadInit};
+
+pub struct AesGcm<A: BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit>(
+    aes_gcm::AesGcm<A, U12>,
+);
+
+impl<A: BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit> crypto_provider::aead::AesGcm
+    for AesGcm<A>
+{
+}
+
+impl<K: crypto_provider::aes::AesKey, A: BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit>
+    AeadInit<K> for AesGcm<A>
+{
+    fn new(key: &K) -> Self {
+        Self(aes_gcm::AesGcm::<A, U12>::new(key.as_slice().into()))
+    }
+}
+
+impl<A: aes::cipher::BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit> Aead for AesGcm<A> {
+    const TAG_SIZE: usize = 16;
+    type Nonce = [u8; 12];
+    type Tag = [u8; 16];
+
+    #[cfg(feature = "alloc")]
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0
+            .encrypt(aes_gcm::Nonce::from_slice(nonce), Payload { msg, aad })
+            .map_err(|_| AeadError)
+    }
+
+    fn encrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError> {
+        self.0
+            .encrypt_in_place_detached(aes_gcm::Nonce::from_slice(nonce), aad, msg)
+            .map(|arr| arr.into())
+            .map_err(|_| AeadError)
+    }
+
+    #[cfg(feature = "alloc")]
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0
+            .decrypt(aes_gcm::Nonce::from_slice(nonce), Payload { msg, aad })
+            .map_err(|_| AeadError)
+    }
+
+    fn decrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+        tag: &Self::Tag,
+    ) -> Result<(), AeadError> {
+        self.0
+            .decrypt_in_place_detached(aes_gcm::Nonce::from_slice(nonce), aad, msg, tag.into())
+            .map_err(|_| AeadError)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::marker::PhantomData;
+
+    use crypto_provider_test::aead::aes_gcm::*;
+    use crypto_provider_test::aes::*;
+
+    use super::*;
+
+    #[apply(aes_128_gcm_test_cases)]
+    fn aes_gcm_128_test(testcase: CryptoProviderTestCase<AesGcm<aes::Aes128>>) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_128_gcm_test_cases_detached)]
+    fn aes_128_gcm_test_detached(testcase: CryptoProviderTestCase<AesGcm<aes::Aes128>>) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_256_gcm_test_cases)]
+    fn aes_gcm_256_test(testcase: CryptoProviderTestCase<AesGcm<aes::Aes256>>) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_256_gcm_test_cases_detached)]
+    fn aes_256_gcm_test_detached(testcase: CryptoProviderTestCase<AesGcm<aes::Aes256>>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm_siv.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm_siv.rs
index 402c2ed..408017f 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm_siv.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/aead/aes_gcm_siv.rs
@@ -12,74 +12,118 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use aes_gcm_siv::{AeadInPlace, Aes128GcmSiv, Aes256GcmSiv, KeyInit, Nonce};
+#[cfg(feature = "alloc")]
 extern crate alloc;
+#[cfg(feature = "alloc")]
+use aead::Payload;
+#[cfg(feature = "alloc")]
 use alloc::vec::Vec;
-use crypto_provider::aead::{Aead, AeadError};
 
-use crypto_provider::aead::aes_gcm_siv::AesGcmSiv;
-use crypto_provider::aes::{Aes128Key, Aes256Key, AesKey};
+// RustCrypto defined traits and types
+use aes::cipher::typenum::consts::U16;
+use aes::cipher::BlockCipher;
+use aes::cipher::BlockEncrypt;
+#[cfg(feature = "alloc")]
+use aes_gcm_siv::aead::Aead as _;
+use aes_gcm_siv::aead::KeyInit;
+use aes_gcm_siv::AeadInPlace as _;
 
-pub struct AesGcmSiv128(Aes128GcmSiv);
+// CryptoProvider traits and types
+use crypto_provider::aead::{Aead, AeadError, AeadInit};
 
-impl AesGcmSiv for AesGcmSiv128 {}
+pub struct AesGcmSiv<A: BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit>(
+    aes_gcm_siv::AesGcmSiv<A>,
+);
 
-impl Aead for AesGcmSiv128 {
-    const TAG_SIZE: usize = 16;
-    type Nonce = [u8; 12];
-    type Key = Aes128Key;
+impl<A: BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit> crypto_provider::aead::AesGcmSiv
+    for AesGcmSiv<A>
+{
+}
 
-    fn new(key: &Self::Key) -> Self {
-        Self(Aes128GcmSiv::new(key.as_slice().into()))
-    }
-
-    fn encrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
-        self.0.encrypt_in_place(Nonce::from_slice(nonce), aad, msg).map_err(|_| AeadError)
-    }
-
-    fn decrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
-        self.0.decrypt_in_place(Nonce::from_slice(nonce), aad, msg).map_err(|_| AeadError)
+impl<K: crypto_provider::aes::AesKey, A: BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit>
+    AeadInit<K> for AesGcmSiv<A>
+{
+    fn new(key: &K) -> Self {
+        Self(aes_gcm_siv::AesGcmSiv::<A>::new(key.as_slice().into()))
     }
 }
 
-pub struct AesGcmSiv256(Aes256GcmSiv);
-
-impl AesGcmSiv for AesGcmSiv256 {}
-
-impl Aead for AesGcmSiv256 {
+impl<A: aes::cipher::BlockCipher<BlockSize = U16> + BlockEncrypt + KeyInit> Aead for AesGcmSiv<A> {
     const TAG_SIZE: usize = 16;
     type Nonce = [u8; 12];
-    type Key = Aes256Key;
+    type Tag = [u8; 16];
 
-    fn new(key: &Self::Key) -> Self {
-        Self(Aes256GcmSiv::new(key.as_slice().into()))
+    #[cfg(feature = "alloc")]
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0
+            .encrypt(aes_gcm_siv::Nonce::from_slice(nonce), Payload { msg, aad })
+            .map_err(|_| AeadError)
     }
 
-    fn encrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
-        self.0.encrypt_in_place(Nonce::from_slice(nonce), aad, msg).map_err(|_| AeadError)
+    fn encrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError> {
+        self.0
+            .encrypt_in_place_detached(aes_gcm_siv::Nonce::from_slice(nonce), aad, msg)
+            .map(|arr| arr.into())
+            .map_err(|_| AeadError)
     }
 
-    fn decrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
-        self.0.decrypt_in_place(Nonce::from_slice(nonce), aad, msg).map_err(|_| AeadError)
+    #[cfg(feature = "alloc")]
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &Self::Nonce) -> Result<Vec<u8>, AeadError> {
+        self.0
+            .decrypt(aes_gcm_siv::Nonce::from_slice(nonce), Payload { msg, aad })
+            .map_err(|_| AeadError)
+    }
+
+    fn decrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+        tag: &Self::Tag,
+    ) -> Result<(), AeadError> {
+        self.0
+            .decrypt_in_place_detached(aes_gcm_siv::Nonce::from_slice(nonce), aad, msg, tag.into())
+            .map_err(|_| AeadError)
     }
 }
 
 #[cfg(test)]
 mod tests {
     use core::marker::PhantomData;
-
     use crypto_provider_test::aead::aes_gcm_siv::*;
     use crypto_provider_test::aes::*;
-
-    use super::*;
+    use crypto_provider_test::prelude::apply;
 
     #[apply(aes_128_gcm_siv_test_cases)]
-    fn aes_gcm_siv_128_test(testcase: CryptoProviderTestCase<AesGcmSiv128>) {
+    fn aes_gcm_siv_128_test(
+        testcase: CryptoProviderTestCase<crate::aead::aes_gcm_siv::AesGcmSiv<aes::Aes128>>,
+    ) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_128_gcm_siv_test_cases_detached)]
+    fn aes_gcm_siv_128_test_detached(
+        testcase: CryptoProviderTestCase<crate::aead::aes_gcm_siv::AesGcmSiv<aes::Aes128>>,
+    ) {
         testcase(PhantomData);
     }
 
     #[apply(aes_256_gcm_siv_test_cases)]
-    fn aes_gcm_siv_256_test(testcase: CryptoProviderTestCase<AesGcmSiv256>) {
+    fn aes_gcm_siv_256_test(
+        testcase: CryptoProviderTestCase<crate::aead::aes_gcm_siv::AesGcmSiv<aes::Aes256>>,
+    ) {
+        testcase(PhantomData);
+    }
+
+    #[apply(aes_256_gcm_siv_test_cases_detached)]
+    fn aes_gcm_siv_256_test_detached(
+        testcase: CryptoProviderTestCase<crate::aead::aes_gcm_siv::AesGcmSiv<aes::Aes256>>,
+    ) {
         testcase(PhantomData);
     }
 }
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aead/mod.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aead/mod.rs
index 7fc561b..e4c2c60 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/aead/mod.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/aead/mod.rs
@@ -13,3 +13,5 @@
 // limitations under the License.
 
 pub(crate) mod aes_gcm_siv;
+
+pub(crate) mod aes_gcm;
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aes/cbc.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aes/cbc.rs
deleted file mode 100644
index 06d7224..0000000
--- a/nearby/crypto/crypto_provider_rustcrypto/src/aes/cbc.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-extern crate alloc;
-use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
-use aes::Aes256;
-use alloc::vec::Vec;
-use crypto_provider::aes::{
-    cbc::{AesCbcIv, DecryptionError},
-    Aes256Key, AesKey,
-};
-
-/// RustCrypto implementation of AES-CBC with PKCS7 padding
-pub enum AesCbcPkcs7Padded {}
-impl crypto_provider::aes::cbc::AesCbcPkcs7Padded for AesCbcPkcs7Padded {
-    fn encrypt(key: &Aes256Key, iv: &AesCbcIv, message: &[u8]) -> Vec<u8> {
-        let encryptor = cbc::Encryptor::<Aes256>::new(key.as_array().into(), iv.into());
-        encryptor.encrypt_padded_vec_mut::<Pkcs7>(message)
-    }
-
-    fn decrypt(
-        key: &Aes256Key,
-        iv: &AesCbcIv,
-        ciphertext: &[u8],
-    ) -> Result<Vec<u8>, DecryptionError> {
-        cbc::Decryptor::<Aes256>::new(key.as_array().into(), iv.into())
-            .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
-            .map_err(|_| DecryptionError::BadPadding)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::AesCbcPkcs7Padded;
-    use core::marker::PhantomData;
-    use crypto_provider_test::aes::cbc::*;
-
-    #[apply(aes_256_cbc_test_cases)]
-    fn aes_256_cbc_test(testcase: CryptoProviderTestCase<AesCbcPkcs7Padded>) {
-        testcase(PhantomData);
-    }
-}
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/cbc.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/cbc.rs
new file mode 100644
index 0000000..c6d74f0
--- /dev/null
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/cbc.rs
@@ -0,0 +1,102 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+
+use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
+use aes::Aes256;
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+use crypto_provider::{
+    aes::{
+        cbc::{AesCbcIv, DecryptionError, EncryptionError},
+        Aes256Key, AesKey,
+    },
+    tinyvec::SliceVec,
+};
+
+/// RustCrypto implementation of AES-CBC with PKCS7 padding
+pub enum AesCbcPkcs7Padded {}
+impl crypto_provider::aes::cbc::AesCbcPkcs7Padded for AesCbcPkcs7Padded {
+    #[cfg(feature = "alloc")]
+    fn encrypt(key: &Aes256Key, iv: &AesCbcIv, message: &[u8]) -> Vec<u8> {
+        let encryptor = cbc::Encryptor::<Aes256>::new(key.as_array().into(), iv.into());
+        encryptor.encrypt_padded_vec_mut::<Pkcs7>(message)
+    }
+
+    fn encrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        message: &mut SliceVec<u8>,
+    ) -> Result<(), EncryptionError> {
+        let encryptor = cbc::Encryptor::<Aes256>::new(key.as_array().into(), iv.into());
+        let message_len = message.len();
+        // Set the length so encrypt_padded_mut can write using the full capacity
+        // (Unlike `Vec.set_len`, `SliceVec.set_len` is safe and won't panic if len <= capacity)
+        message.set_len(message.capacity());
+        encryptor
+            .encrypt_padded_mut::<Pkcs7>(message, message_len)
+            .map(|result| result.len())
+            // `SliceVec.set_len` is safe, and won't panic because `encrypt_padded_mut` never
+            // returns a slice longer than the given buffer.
+            .map(|new_len| message.set_len(new_len))
+            .map_err(|_| {
+                message.set_len(message_len); // Set the buffer back to its original length
+                EncryptionError::PaddingFailed
+            })
+    }
+
+    #[cfg(feature = "alloc")]
+    fn decrypt(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &[u8],
+    ) -> Result<Vec<u8>, DecryptionError> {
+        cbc::Decryptor::<Aes256>::new(key.as_array().into(), iv.into())
+            .decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
+            .map_err(|_| DecryptionError::BadPadding)
+    }
+
+    fn decrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &mut SliceVec<u8>,
+    ) -> Result<(), DecryptionError> {
+        // Decrypted size is always smaller than the input size because of padding, so we don't need
+        // to set the length to the full capacity.
+        cbc::Decryptor::<Aes256>::new(key.as_array().into(), iv.into())
+            .decrypt_padded_mut::<Pkcs7>(ciphertext)
+            .map(|result| result.len())
+            // `SliceVec.set_len` is safe, and won't panic because decrypted result length is always
+            // smaller than the input size.
+            .map(|new_len| ciphertext.set_len(new_len))
+            .map_err(|_| {
+                ciphertext.as_mut().fill(0);
+                DecryptionError::BadPadding
+            })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::AesCbcPkcs7Padded;
+    use core::marker::PhantomData;
+    use crypto_provider_test::aes::cbc::*;
+
+    #[apply(aes_256_cbc_test_cases)]
+    fn aes_256_cbc_test(testcase: CryptoProviderTestCase<AesCbcPkcs7Padded>) {
+        testcase(PhantomData);
+    }
+}
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aes/ctr.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/ctr.rs
similarity index 88%
rename from nearby/crypto/crypto_provider_rustcrypto/src/aes/ctr.rs
rename to nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/ctr.rs
index 85cdac6..09ae557 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/aes/ctr.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/ctr.rs
@@ -33,11 +33,7 @@
         }
     }
 
-    fn encrypt(&mut self, data: &mut [u8]) {
-        self.cipher.apply_keystream(data);
-    }
-
-    fn decrypt(&mut self, data: &mut [u8]) {
+    fn apply_keystream(&mut self, data: &mut [u8]) {
         self.cipher.apply_keystream(data);
     }
 }
@@ -59,11 +55,7 @@
         }
     }
 
-    fn encrypt(&mut self, data: &mut [u8]) {
-        self.cipher.apply_keystream(data);
-    }
-
-    fn decrypt(&mut self, data: &mut [u8]) {
+    fn apply_keystream(&mut self, data: &mut [u8]) {
         self.cipher.apply_keystream(data);
     }
 }
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/aes/mod.rs b/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/mod.rs
similarity index 98%
rename from nearby/crypto/crypto_provider_rustcrypto/src/aes/mod.rs
rename to nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/mod.rs
index c71f2ec..4c04bf5 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/aes/mod.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/aes_cp/mod.rs
@@ -22,7 +22,6 @@
 };
 
 /// Module implementing AES-CBC.
-#[cfg(feature = "alloc")]
 pub(crate) mod cbc;
 
 pub(crate) mod ctr;
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/ed25519.rs b/nearby/crypto/crypto_provider_rustcrypto/src/ed25519.rs
index d11a5ea..ce8089e 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/ed25519.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/ed25519.rs
@@ -15,8 +15,8 @@
 use ed25519_dalek::Signer;
 
 use crypto_provider::ed25519::{
-    InvalidBytes, RawPrivateKey, RawPublicKey, RawSignature, Signature as _, SignatureError,
-    PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH,
+    InvalidBytes, RawPrivateKey, RawPrivateKeyPermit, RawPublicKey, RawSignature, Signature as _,
+    SignatureError, PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH,
 };
 
 pub struct Ed25519;
@@ -33,11 +33,11 @@
     type PublicKey = PublicKey;
     type Signature = Signature;
 
-    fn private_key(&self) -> [u8; PRIVATE_KEY_LENGTH] {
+    fn raw_private_key(&self, _permit: &RawPrivateKeyPermit) -> [u8; PRIVATE_KEY_LENGTH] {
         self.0.to_bytes()
     }
 
-    fn from_private_key(bytes: &RawPrivateKey) -> Self {
+    fn from_raw_private_key(bytes: &RawPrivateKey, _permit: &RawPrivateKeyPermit) -> Self {
         Self(ed25519_dalek::SigningKey::from_bytes(bytes))
     }
 
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/hkdf_rc.rs b/nearby/crypto/crypto_provider_rustcrypto/src/hkdf_cp.rs
similarity index 86%
rename from nearby/crypto/crypto_provider_rustcrypto/src/hkdf_rc.rs
rename to nearby/crypto/crypto_provider_rustcrypto/src/hkdf_cp.rs
index aba0d4a..48380fa 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/hkdf_rc.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/hkdf_cp.rs
@@ -22,7 +22,7 @@
 use hmac::digest::{HashMarker, OutputSizeUser};
 
 /// RustCrypto based hkdf implementation
-pub struct Hkdf<D>
+pub struct Hkdf<D>(hkdf::Hkdf<D>)
 where
     D: OutputSizeUser,
     D: CoreProxy,
@@ -33,10 +33,7 @@
         + Default
         + Clone,
     <D::Core as BlockSizeUser>::BlockSize: IsLess<U256>,
-    Le<<D::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
-{
-    hkdf_impl: hkdf::Hkdf<D>,
-}
+    Le<<D::Core as BlockSizeUser>::BlockSize, U256>: NonZero;
 
 impl<D> crypto_provider::hkdf::Hkdf for Hkdf<D>
 where
@@ -52,7 +49,7 @@
     Le<<D::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
 {
     fn new(salt: Option<&[u8]>, ikm: &[u8]) -> Self {
-        Hkdf { hkdf_impl: hkdf::Hkdf::new(salt, ikm) }
+        Hkdf(hkdf::Hkdf::new(salt, ikm))
     }
 
     fn expand_multi_info(
@@ -60,11 +57,11 @@
         info_components: &[&[u8]],
         okm: &mut [u8],
     ) -> Result<(), InvalidLength> {
-        self.hkdf_impl.expand_multi_info(info_components, okm).map_err(|_| InvalidLength)
+        self.0.expand_multi_info(info_components, okm).map_err(|_| InvalidLength)
     }
 
     fn expand(&self, info: &[u8], okm: &mut [u8]) -> Result<(), InvalidLength> {
-        self.hkdf_impl.expand(info, okm).map_err(|_| InvalidLength)
+        self.0.expand(info, okm).map_err(|_| InvalidLength)
     }
 }
 
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/hmac_rc.rs b/nearby/crypto/crypto_provider_rustcrypto/src/hmac_cp.rs
similarity index 72%
rename from nearby/crypto/crypto_provider_rustcrypto/src/hmac_rc.rs
rename to nearby/crypto/crypto_provider_rustcrypto/src/hmac_cp.rs
index dfda208..d8cef33 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/hmac_rc.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/hmac_cp.rs
@@ -23,7 +23,7 @@
 use hmac::Mac;
 
 /// RustCrypto based hmac implementation
-pub struct Hmac<D>
+pub struct Hmac<D>(hmac::Hmac<D>)
 where
     D: OutputSizeUser,
     D: CoreProxy,
@@ -34,43 +34,38 @@
         + Default
         + Clone,
     <D::Core as BlockSizeUser>::BlockSize: IsLess<U256>,
-    Le<<D::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
-{
-    hmac_impl: hmac::Hmac<D>,
-}
+    Le<<D::Core as BlockSizeUser>::BlockSize, U256>: NonZero;
 
 impl crypto_provider::hmac::Hmac<32> for Hmac<sha2::Sha256> {
     #[allow(clippy::expect_used)]
     fn new_from_key(key: [u8; 32]) -> Self {
         hmac::Hmac::new_from_slice(&key)
-            .map(|hmac| Self { hmac_impl: hmac })
+            .map(Self)
             .expect("length will always be valid because input key is of fixed size")
     }
 
     fn new_from_slice(key: &[u8]) -> Result<Self, InvalidLength> {
-        hmac::Hmac::new_from_slice(key)
-            .map(|hmac| Self { hmac_impl: hmac })
-            .map_err(|_| InvalidLength)
+        hmac::Hmac::new_from_slice(key).map(Self).map_err(|_| InvalidLength)
     }
 
     fn update(&mut self, data: &[u8]) {
-        self.hmac_impl.update(data);
+        self.0.update(data);
     }
 
     fn finalize(self) -> [u8; 32] {
-        self.hmac_impl.finalize().into_bytes().into()
+        self.0.finalize().into_bytes().into()
     }
 
     fn verify_slice(self, tag: &[u8]) -> Result<(), MacError> {
-        self.hmac_impl.verify_slice(tag).map_err(|_| MacError)
+        self.0.verify_slice(tag).map_err(|_| MacError)
     }
 
     fn verify(self, tag: [u8; 32]) -> Result<(), MacError> {
-        self.hmac_impl.verify(&tag.into()).map_err(|_| MacError)
+        self.0.verify(&tag.into()).map_err(|_| MacError)
     }
 
     fn verify_truncated_left(self, tag: &[u8]) -> Result<(), MacError> {
-        self.hmac_impl.verify_truncated_left(tag).map_err(|_| MacError)
+        self.0.verify_truncated_left(tag).map_err(|_| MacError)
     }
 }
 
@@ -78,34 +73,32 @@
     #[allow(clippy::expect_used)]
     fn new_from_key(key: [u8; 64]) -> Self {
         hmac::Hmac::new_from_slice(&key)
-            .map(|hmac| Self { hmac_impl: hmac })
+            .map(Self)
             .expect("length will always be valid because input key is of fixed size")
     }
 
     fn new_from_slice(key: &[u8]) -> Result<Self, InvalidLength> {
-        hmac::Hmac::new_from_slice(key)
-            .map(|hmac| Self { hmac_impl: hmac })
-            .map_err(|_| InvalidLength)
+        hmac::Hmac::new_from_slice(key).map(Self).map_err(|_| InvalidLength)
     }
 
     fn update(&mut self, data: &[u8]) {
-        self.hmac_impl.update(data);
+        self.0.update(data);
     }
 
     fn finalize(self) -> [u8; 64] {
-        self.hmac_impl.finalize().into_bytes().into()
+        self.0.finalize().into_bytes().into()
     }
 
     fn verify_slice(self, tag: &[u8]) -> Result<(), MacError> {
-        self.hmac_impl.verify_slice(tag).map_err(|_| MacError)
+        self.0.verify_slice(tag).map_err(|_| MacError)
     }
 
     fn verify(self, tag: [u8; 64]) -> Result<(), MacError> {
-        self.hmac_impl.verify(&tag.into()).map_err(|_| MacError)
+        self.0.verify(&tag.into()).map_err(|_| MacError)
     }
 
     fn verify_truncated_left(self, tag: &[u8]) -> Result<(), MacError> {
-        self.hmac_impl.verify_truncated_left(tag).map_err(|_| MacError)
+        self.0.verify_truncated_left(tag).map_err(|_| MacError)
     }
 }
 
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/lib.rs b/nearby/crypto/crypto_provider_rustcrypto/src/lib.rs
index e95712c..628f679 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/lib.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/lib.rs
@@ -25,6 +25,7 @@
 
 use core::{fmt::Debug, marker::PhantomData};
 
+pub use aes;
 use cfg_if::cfg_if;
 pub use hkdf;
 pub use hmac;
@@ -35,17 +36,17 @@
 /// Contains the RustCrypto backed impls for AES-GCM-SIV operations
 mod aead;
 /// Contains the RustCrypto backed AES impl for CryptoProvider
-pub mod aes;
+pub mod aes_cp;
 /// Contains the RustCrypto backed impl for ed25519 key generation, signing, and verification
 mod ed25519;
 /// Contains the RustCrypto backed hkdf impl for CryptoProvider
-mod hkdf_rc;
+mod hkdf_cp;
 /// Contains the RustCrypto backed hmac impl for CryptoProvider
-mod hmac_rc;
+mod hmac_cp;
 /// Contains the RustCrypto backed P256 impl for CryptoProvider
 mod p256;
 /// Contains the RustCrypto backed SHA2 impl for CryptoProvider
-mod sha2_rc;
+mod sha2_cp;
 /// Contains the RustCrypto backed X25519 impl for CryptoProvider
 mod x25519;
 
@@ -54,9 +55,11 @@
         /// Providing a type alias for compatibility with existing usage of RustCrypto
         /// by default we use StdRng for the underlying csprng
         pub type RustCrypto = RustCryptoImpl<rand::rngs::StdRng>;
-    } else {
+    } else if #[cfg(feature = "rand_chacha")] {
         /// A no_std compatible implementation of CryptoProvider backed by RustCrypto crates
         pub type RustCrypto = RustCryptoImpl<rand_chacha::ChaCha20Rng>;
+    } else {
+        compile_error!("Must specify either --features std or --features rand_chacha");
     }
 }
 
@@ -76,23 +79,24 @@
 impl<R: CryptoRng + SeedableRng + RngCore + Eq + PartialEq + Debug + Clone + Send>
     crypto_provider::CryptoProvider for RustCryptoImpl<R>
 {
-    type HkdfSha256 = hkdf_rc::Hkdf<sha2::Sha256>;
-    type HmacSha256 = hmac_rc::Hmac<sha2::Sha256>;
-    type HkdfSha512 = hkdf_rc::Hkdf<sha2::Sha512>;
-    type HmacSha512 = hmac_rc::Hmac<sha2::Sha512>;
-    #[cfg(feature = "alloc")]
-    type AesCbcPkcs7Padded = aes::cbc::AesCbcPkcs7Padded;
+    type HkdfSha256 = hkdf_cp::Hkdf<sha2::Sha256>;
+    type HmacSha256 = hmac_cp::Hmac<sha2::Sha256>;
+    type HkdfSha512 = hkdf_cp::Hkdf<sha2::Sha512>;
+    type HmacSha512 = hmac_cp::Hmac<sha2::Sha512>;
+    type AesCbcPkcs7Padded = aes_cp::cbc::AesCbcPkcs7Padded;
     type X25519 = x25519::X25519Ecdh<R>;
     type P256 = p256::P256Ecdh<R>;
-    type Sha256 = sha2_rc::RustCryptoSha256;
-    type Sha512 = sha2_rc::RustCryptoSha512;
-    type Aes128 = aes::Aes128;
-    type Aes256 = aes::Aes256;
-    type AesCtr128 = aes::ctr::AesCtr128;
-    type AesCtr256 = aes::ctr::AesCtr256;
+    type Sha256 = sha2_cp::RustCryptoSha256;
+    type Sha512 = sha2_cp::RustCryptoSha512;
+    type Aes128 = aes_cp::Aes128;
+    type Aes256 = aes_cp::Aes256;
+    type AesCtr128 = aes_cp::ctr::AesCtr128;
+    type AesCtr256 = aes_cp::ctr::AesCtr256;
     type Ed25519 = ed25519::Ed25519;
-    type Aes128GcmSiv = aead::aes_gcm_siv::AesGcmSiv128;
-    type Aes256GcmSiv = aead::aes_gcm_siv::AesGcmSiv256;
+    type Aes128GcmSiv = aead::aes_gcm_siv::AesGcmSiv<aes::Aes128>;
+    type Aes256GcmSiv = aead::aes_gcm_siv::AesGcmSiv<aes::Aes256>;
+    type Aes128Gcm = aead::aes_gcm::AesGcm<aes::Aes128>;
+    type Aes256Gcm = aead::aes_gcm::AesGcm<aes::Aes256>;
     type CryptoRng = RcRng<R>;
 
     fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
@@ -124,6 +128,7 @@
 mod tests {
     use core::marker::PhantomData;
 
+    use crypto_provider_test::prelude::*;
     use crypto_provider_test::sha2::*;
 
     use crate::RustCrypto;
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/p256.rs b/nearby/crypto/crypto_provider_rustcrypto/src/p256.rs
index 539ccc0..98a6cd4 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/p256.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/p256.rs
@@ -12,14 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-extern crate alloc;
-
 use crate::RcRng;
-use alloc::vec::Vec;
 use core::marker::PhantomData;
 use crypto_provider::{
     elliptic_curve::{EcdhProvider, EphemeralSecret},
-    p256::P256,
+    p256::{PointCompression, P256},
+    tinyvec::ArrayVec,
 };
 use p256::{
     elliptic_curve,
@@ -52,8 +50,12 @@
         p256::PublicKey::from_sec1_bytes(bytes).map(Self)
     }
 
-    fn to_sec1_bytes(&self) -> Vec<u8> {
-        self.0.to_encoded_point(true).as_bytes().to_vec()
+    fn to_sec1_bytes(&self, point_compression: PointCompression) -> ArrayVec<[u8; 65]> {
+        let mut bytes = ArrayVec::<[u8; 65]>::new();
+        bytes.extend_from_slice(
+            self.0.to_encoded_point(point_compression == PointCompression::Compressed).as_bytes(),
+        );
+        bytes
     }
 
     #[allow(clippy::expect_used)]
@@ -87,6 +89,7 @@
     type Impl = P256Ecdh<R>;
     type Error = sec1::Error;
     type Rng = RcRng<R>;
+    type EncodedPublicKey = ArrayVec<[u8; 65]>;
 
     fn generate_random(rng: &mut Self::Rng) -> Self {
         Self {
@@ -95,8 +98,10 @@
         }
     }
 
-    fn public_key_bytes(&self) -> Vec<u8> {
-        self.secret.public_key().to_encoded_point(false).as_bytes().into()
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
+        let mut bytes = Self::EncodedPublicKey::new();
+        bytes.extend_from_slice(self.secret.public_key().to_encoded_point(false).as_bytes());
+        bytes
     }
 
     fn diffie_hellman(
@@ -136,7 +141,7 @@
     use rand::rngs::StdRng;
 
     #[apply(p256_test_cases)]
-    fn p256_tests(testcase: CryptoProviderTestCase<P256Ecdh<StdRng>>) {
+    fn p256_tests(testcase: CryptoProviderTestCase<P256Ecdh<StdRng>>, _name: &str) {
         testcase(PhantomData::<P256Ecdh<StdRng>>)
     }
 }
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/sha2_rc.rs b/nearby/crypto/crypto_provider_rustcrypto/src/sha2_cp.rs
similarity index 100%
rename from nearby/crypto/crypto_provider_rustcrypto/src/sha2_rc.rs
rename to nearby/crypto/crypto_provider_rustcrypto/src/sha2_cp.rs
diff --git a/nearby/crypto/crypto_provider_rustcrypto/src/x25519.rs b/nearby/crypto/crypto_provider_rustcrypto/src/x25519.rs
index 445e858..ad67777 100644
--- a/nearby/crypto/crypto_provider_rustcrypto/src/x25519.rs
+++ b/nearby/crypto/crypto_provider_rustcrypto/src/x25519.rs
@@ -12,10 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-extern crate alloc;
-
 use crate::RcRng;
-use alloc::vec::Vec;
 use core::marker::PhantomData;
 use crypto_provider::elliptic_curve::{EcdhProvider, EphemeralSecret, PublicKey};
 use crypto_provider::x25519::X25519;
@@ -45,6 +42,7 @@
     type Impl = X25519Ecdh<R>;
     type Error = Error;
     type Rng = RcRng<R>;
+    type EncodedPublicKey = [u8; 32];
 
     fn generate_random(rng: &mut Self::Rng) -> Self {
         Self {
@@ -53,9 +51,9 @@
         }
     }
 
-    fn public_key_bytes(&self) -> Vec<u8> {
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
         let pubkey: x25519_dalek::PublicKey = (&self.secret).into();
-        pubkey.to_bytes().into()
+        pubkey.to_bytes()
     }
 
     fn diffie_hellman(
@@ -90,14 +88,15 @@
 
 impl PublicKey<X25519> for X25519PublicKey {
     type Error = Error;
+    type EncodedPublicKey = [u8; 32];
 
     fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
         let byte_sized: [u8; 32] = bytes.try_into().map_err(|_| Error::WrongSize)?;
         Ok(Self(byte_sized.into()))
     }
 
-    fn to_bytes(&self) -> Vec<u8> {
-        self.0.as_bytes().to_vec()
+    fn to_bytes(&self) -> Self::EncodedPublicKey {
+        self.0.to_bytes()
     }
 }
 
diff --git a/nearby/crypto/crypto_provider_stubs/Cargo.toml b/nearby/crypto/crypto_provider_stubs/Cargo.toml
index d7c4d45..4e8bdec 100644
--- a/nearby/crypto/crypto_provider_stubs/Cargo.toml
+++ b/nearby/crypto/crypto_provider_stubs/Cargo.toml
@@ -5,4 +5,4 @@
 publish.workspace = true
 
 [dependencies]
-crypto_provider = {workspace = true, features = ["std", "alloc"] }
\ No newline at end of file
+crypto_provider = {workspace = true, features = ["std", "alloc"] }
diff --git a/nearby/crypto/crypto_provider_stubs/src/lib.rs b/nearby/crypto/crypto_provider_stubs/src/lib.rs
index 5d54b72..32c9381 100644
--- a/nearby/crypto/crypto_provider_stubs/src/lib.rs
+++ b/nearby/crypto/crypto_provider_stubs/src/lib.rs
@@ -20,21 +20,23 @@
 
 use std::fmt::Debug;
 
-use crypto_provider::ed25519::{RawPrivateKey, RawPublicKey, RawSignature};
+use crypto_provider::aead::AeadInit;
 use crypto_provider::{
-    aead::aes_gcm_siv::AesGcmSiv,
-    aead::{Aead, AeadError},
+    aead::{Aead, AeadError, AesGcm, AesGcmSiv},
     aes::{
-        cbc::{AesCbcIv, AesCbcPkcs7Padded, DecryptionError},
+        cbc::{AesCbcIv, AesCbcPkcs7Padded, DecryptionError, EncryptionError},
         ctr::{AesCtr, NonceAndCounter},
         Aes, Aes128Key, Aes256Key, AesBlock, AesCipher, AesDecryptCipher, AesEncryptCipher,
     },
-    ed25519,
-    ed25519::{Ed25519Provider, InvalidBytes, KeyPair, Signature, SignatureError},
+    ed25519::{
+        self, Ed25519Provider, InvalidBytes, KeyPair, RawPrivateKey, RawPrivateKeyPermit,
+        RawPublicKey, RawSignature, Signature, SignatureError,
+    },
     elliptic_curve::{EcdhProvider, EphemeralSecret, PublicKey},
     hkdf::{Hkdf, InvalidLength},
     hmac::{Hmac, MacError},
-    p256::{P256PublicKey, P256},
+    p256::{P256PublicKey, PointCompression, P256},
+    tinyvec::{ArrayVec, SliceVec},
     x25519::X25519,
 };
 
@@ -58,6 +60,8 @@
     type Ed25519 = Ed25519Stubs;
     type Aes128GcmSiv = Aes128Stubs;
     type Aes256GcmSiv = Aes256Stubs;
+    type Aes128Gcm = Aes128Stubs;
+    type Aes256Gcm = Aes256Stubs;
     type CryptoRng = ();
 
     fn constant_time_eq(_a: &[u8], _b: &[u8]) -> bool {
@@ -171,6 +175,14 @@
         unimplemented!()
     }
 
+    fn encrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        message: &mut SliceVec<u8>,
+    ) -> Result<(), EncryptionError> {
+        unimplemented!()
+    }
+
     fn decrypt(
         _key: &Aes256Key,
         _iv: &AesCbcIv,
@@ -178,6 +190,14 @@
     ) -> Result<Vec<u8>, DecryptionError> {
         unimplemented!()
     }
+
+    fn decrypt_in_place(
+        key: &Aes256Key,
+        iv: &AesCbcIv,
+        ciphertext: &mut SliceVec<u8>,
+    ) -> Result<(), DecryptionError> {
+        unimplemented!()
+    }
 }
 
 pub struct X25519Stubs;
@@ -194,12 +214,13 @@
     type Impl = X25519Stubs;
     type Error = ();
     type Rng = ();
+    type EncodedPublicKey = [u8; 32];
 
     fn generate_random(_rng: &mut Self::Rng) -> Self {
         unimplemented!()
     }
 
-    fn public_key_bytes(&self) -> Vec<u8> {
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
         unimplemented!()
     }
 
@@ -215,12 +236,13 @@
     type Impl = P256Stubs;
     type Error = ();
     type Rng = ();
+    type EncodedPublicKey = ArrayVec<[u8; 65]>;
 
     fn generate_random(_rng: &mut Self::Rng) -> Self {
         unimplemented!()
     }
 
-    fn public_key_bytes(&self) -> Vec<u8> {
+    fn public_key_bytes(&self) -> Self::EncodedPublicKey {
         unimplemented!()
     }
 
@@ -237,12 +259,13 @@
 
 impl PublicKey<X25519> for EcdhPubKey {
     type Error = ();
+    type EncodedPublicKey = [u8; 32];
 
     fn from_bytes(_bytes: &[u8]) -> Result<Self, Self::Error> {
         unimplemented!()
     }
 
-    fn to_bytes(&self) -> Vec<u8> {
+    fn to_bytes(&self) -> Self::EncodedPublicKey {
         unimplemented!()
     }
 }
@@ -257,7 +280,7 @@
         unimplemented!()
     }
 
-    fn to_sec1_bytes(&self) -> Vec<u8> {
+    fn to_sec1_bytes(&self, _point_compression: PointCompression) -> ArrayVec<[u8; 65]> {
         unimplemented!()
     }
 
@@ -294,6 +317,12 @@
 
 pub struct Aes128Stubs;
 
+impl AeadInit<Aes128Key> for Aes128Stubs {
+    fn new(key: &Aes128Key) -> Self {
+        unimplemented!()
+    }
+}
+
 impl AesCipher for Aes128Stubs {
     type Key = Aes128Key;
 
@@ -321,11 +350,7 @@
         unimplemented!()
     }
 
-    fn encrypt(&mut self, _data: &mut [u8]) {
-        unimplemented!()
-    }
-
-    fn decrypt(&mut self, _data: &mut [u8]) {
+    fn apply_keystream(&mut self, data: &mut [u8]) {
         unimplemented!()
     }
 }
@@ -333,25 +358,48 @@
 impl Aead for Aes128Stubs {
     const TAG_SIZE: usize = 16;
     type Nonce = [u8; 12];
-    type Key = Aes128Key;
+    type Tag = [u8; 16];
 
-    fn new(key: &Self::Key) -> Self {
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &[u8; 12]) -> Result<Vec<u8>, AeadError> {
         unimplemented!()
     }
 
-    fn encrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
+    fn encrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError> {
         unimplemented!()
     }
 
-    fn decrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &[u8; 12]) -> Result<Vec<u8>, AeadError> {
+        unimplemented!()
+    }
+
+    fn decrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+        tag: &Self::Tag,
+    ) -> Result<(), AeadError> {
         unimplemented!()
     }
 }
 
 impl AesGcmSiv for Aes128Stubs {}
 
+impl AesGcm for Aes128Stubs {}
+
 pub struct Aes256Stubs;
 
+impl AeadInit<Aes256Key> for Aes256Stubs {
+    fn new(key: &Aes256Key) -> Self {
+        unimplemented!()
+    }
+}
+
 impl AesCipher for Aes256Stubs {
     type Key = Aes256Key;
 
@@ -379,11 +427,7 @@
         unimplemented!()
     }
 
-    fn encrypt(&mut self, _data: &mut [u8]) {
-        unimplemented!()
-    }
-
-    fn decrypt(&mut self, _data: &mut [u8]) {
+    fn apply_keystream(&mut self, data: &mut [u8]) {
         unimplemented!()
     }
 }
@@ -391,23 +435,40 @@
 impl Aead for Aes256Stubs {
     const TAG_SIZE: usize = 16;
     type Nonce = [u8; 12];
-    type Key = Aes256Key;
+    type Tag = [u8; 16];
 
-    fn new(key: &Self::Key) -> Self {
+    fn encrypt(&self, msg: &[u8], aad: &[u8], nonce: &[u8; 12]) -> Result<Vec<u8>, AeadError> {
         unimplemented!()
     }
 
-    fn encrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
+    fn encrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+    ) -> Result<Self::Tag, AeadError> {
         unimplemented!()
     }
 
-    fn decrypt(&self, msg: &mut Vec<u8>, aad: &[u8], nonce: &[u8; 12]) -> Result<(), AeadError> {
+    fn decrypt(&self, msg: &[u8], aad: &[u8], nonce: &[u8; 12]) -> Result<Vec<u8>, AeadError> {
+        unimplemented!()
+    }
+
+    fn decrypt_detached(
+        &self,
+        msg: &mut [u8],
+        aad: &[u8],
+        nonce: &Self::Nonce,
+        tag: &Self::Tag,
+    ) -> Result<(), AeadError> {
         unimplemented!()
     }
 }
 
 impl AesGcmSiv for Aes256Stubs {}
 
+impl AesGcm for Aes256Stubs {}
+
 pub struct Ed25519Stubs;
 
 impl Ed25519Provider for Ed25519Stubs {
@@ -457,11 +518,11 @@
     type PublicKey = PublicKeyStubs;
     type Signature = SignatureStubs;
 
-    fn private_key(&self) -> RawPrivateKey {
+    fn raw_private_key(&self, _permit: &RawPrivateKeyPermit) -> RawPrivateKey {
         unimplemented!()
     }
 
-    fn from_private_key(_bytes: &RawPrivateKey) -> Self
+    fn from_raw_private_key(_bytes: &RawPrivateKey, _permit: &RawPrivateKeyPermit) -> Self
     where
         Self: Sized,
     {
diff --git a/nearby/crypto/crypto_provider_test/Cargo.toml b/nearby/crypto/crypto_provider_test/Cargo.toml
index a4d92ec..4064330 100644
--- a/nearby/crypto/crypto_provider_test/Cargo.toml
+++ b/nearby/crypto/crypto_provider_test/Cargo.toml
@@ -5,7 +5,7 @@
 publish.workspace = true
 
 [dependencies]
-crypto_provider = {  workspace = true, features = ["test_vectors"] }
+crypto_provider = {  workspace = true, features = ["raw_private_key_permit", "test_vectors", "alloc"] }
 rand_ext.workspace = true
 test_helper.workspace = true
 
@@ -14,4 +14,4 @@
 rstest.workspace = true
 rstest_reuse.workspace = true
 wycheproof.workspace = true
-hex.workspace = true
\ No newline at end of file
+hex.workspace = true
diff --git a/nearby/crypto/crypto_provider_test/fuzz/Cargo.lock b/nearby/crypto/crypto_provider_test/fuzz/Cargo.lock
index 9e1d140..bd8f6f9 100644
--- a/nearby/crypto/crypto_provider_test/fuzz/Cargo.lock
+++ b/nearby/crypto/crypto_provider_test/fuzz/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -165,6 +179,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_default"
@@ -193,6 +210,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -239,9 +257,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -307,9 +325,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -390,6 +408,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -732,6 +760,12 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
 name = "typenum"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -773,9 +807,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/crypto/crypto_provider_test/src/aead/aes_gcm.rs b/nearby/crypto/crypto_provider_test/src/aead/aes_gcm.rs
new file mode 100644
index 0000000..88eaf32
--- /dev/null
+++ b/nearby/crypto/crypto_provider_test/src/aead/aes_gcm.rs
@@ -0,0 +1,316 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+use alloc::vec::Vec;
+use core::marker;
+
+use hex_literal::hex;
+use rstest_reuse::template;
+
+pub use crate::prelude;
+use crypto_provider::aead::{AeadInit, AesGcm};
+
+/// Test AES-GCM-128 encryption
+pub fn aes_128_gcm_test_encrypt<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC4
+    let test_key = hex!("bedcfb5a011ebc84600fcb296c15af0d");
+    let nonce = hex!("438a547a94ea88dce46c6c85");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("");
+    let tag = hex!("960247ba5cde02e41a313c4c0136edc3");
+    let result = aes.encrypt(&msg, b"", &nonce).expect("Should succeed");
+    assert_eq!(&result[..], &tag);
+}
+
+pub fn aes_128_gcm_test_encrypt_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC4
+    let test_key = hex!("bedcfb5a011ebc84600fcb296c15af0d");
+    let nonce = hex!("438a547a94ea88dce46c6c85");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("");
+    let tag = hex!("960247ba5cde02e41a313c4c0136edc3");
+    let mut buf = Vec::from(msg.as_slice());
+    let actual_tag: [u8; 16] = aes.encrypt_detached(&mut buf, b"", &nonce).unwrap();
+    assert_eq!(&buf, &[0_u8; 0]);
+    assert_eq!(&actual_tag, &tag);
+}
+
+/// Test AES-GCM-128 decryption
+pub fn aes_128_gcm_test_decrypt<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC2
+    let test_key = hex!("5b9604fe14eadba931b0ccf34843dab9");
+    let nonce = hex!("921d2507fa8007b7bd067d34");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("001d0c231287c1182784554ca3a21908");
+    let ct = hex!("49d8b9783e911913d87094d1f63cc765");
+    let ad = hex!("00112233445566778899aabbccddeeff");
+    let tag = hex!("1e348ba07cca2cf04c618cb4d43a5b92");
+    let result = aes.encrypt(&msg, &ad, &nonce).expect("should succeed");
+    assert_eq!(&result[..16], &ct);
+    assert_eq!(&result[16..], &tag);
+    assert_eq!(A::TAG_SIZE, result[16..].len());
+    let result = aes.decrypt(&result[..], &ad, &nonce).expect("should succeed");
+    assert_eq!(&result[..], &msg);
+}
+
+/// Test AES-GCM-128 decryption
+pub fn aes_128_gcm_test_decrypt_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC2
+    let test_key = hex!("5b9604fe14eadba931b0ccf34843dab9");
+    let nonce = hex!("921d2507fa8007b7bd067d34");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("001d0c231287c1182784554ca3a21908");
+    let ct = hex!("49d8b9783e911913d87094d1f63cc765");
+    let ad = hex!("00112233445566778899aabbccddeeff");
+    let tag = hex!("1e348ba07cca2cf04c618cb4d43a5b92");
+    let mut buf = Vec::from(msg.as_slice());
+    let actual_tag = aes.encrypt_detached(&mut buf, &ad, &nonce).unwrap();
+    assert_eq!(&buf, &ct);
+    assert_eq!(actual_tag, tag);
+    assert!(aes.decrypt_detached(&mut buf, &ad, &nonce, &tag).is_ok());
+    assert_eq!(&buf[..], &msg);
+}
+
+/// Test AES-GCM-128 decryption
+pub fn aes_128_gcm_test_decrypt_detached_bad_tag<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC23
+    let test_key = hex!("000102030405060708090a0b0c0d0e0f");
+    let nonce = hex!("505152535455565758595a5b");
+    let aes = A::new(&test_key.into());
+    let ct = hex!("eb156d081ed6b6b55f4612f021d87b39");
+    let mut buf = Vec::from(ct.as_slice());
+    let bad_tag = hex!("d9847dbc326a06e988c77ad3863e6083");
+    aes.decrypt_detached(&mut buf, b"", &nonce, &bad_tag)
+        .expect_err("decryption tag verification should fail");
+    // assert that the buffer does not change if tag verification fails
+    assert_eq!(buf, ct);
+}
+
+/// Test AES-256-GCM encryption/decryption
+pub fn aes_256_gcm_test_tc74<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC74
+    let test_key = hex!("29d3a44f8723dc640239100c365423a312934ac80239212ac3df3421a2098123");
+    let nonce = hex!("00112233445566778899aabb");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("");
+    let ad = hex!("aabbccddeeff");
+    let tag = hex!("2a7d77fa526b8250cb296078926b5020");
+    let result = aes.encrypt(&msg, &ad, &nonce).expect("should succeed");
+    assert_eq!(&result[..], &tag);
+    assert_eq!(A::TAG_SIZE, result.len());
+    let result = aes.decrypt(&result, &ad, &nonce).expect("should succeed");
+    assert_eq!(&result[..], &msg);
+}
+
+/// Test AES-256-GCM encryption/decryption
+pub fn aes_256_gcm_test_tc74_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC74
+    let test_key = hex!("29d3a44f8723dc640239100c365423a312934ac80239212ac3df3421a2098123");
+    let nonce = hex!("00112233445566778899aabb");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("");
+    let ad = hex!("aabbccddeeff");
+    let ct = hex!("");
+    let tag = hex!("2a7d77fa526b8250cb296078926b5020");
+    let mut buf = Vec::new();
+    buf.extend_from_slice(&msg);
+    let actual_tag = aes.encrypt_detached(&mut buf, &ad, &nonce).unwrap();
+    assert_eq!(&buf, &ct);
+    assert_eq!(&actual_tag, &tag);
+    assert_eq!(A::TAG_SIZE, tag.len());
+    assert!(aes.decrypt_detached(&mut buf, &ad, &nonce, &actual_tag).is_ok());
+    assert_eq!(&buf, &msg);
+}
+
+/// Test AES-256-GCM encryption/decryption
+pub fn aes_256_gcm_test_tc79<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC79
+    let test_key = hex!("59d4eafb4de0cfc7d3db99a8f54b15d7b39f0acc8da69763b019c1699f87674a");
+    let nonce = hex!("2fcb1b38a99e71b84740ad9b");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("549b365af913f3b081131ccb6b825588");
+    let ct = hex!("f58c16690122d75356907fd96b570fca");
+    let tag = hex!("28752c20153092818faba2a334640d6e");
+    let result = aes.encrypt(&msg, b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..16], &ct);
+    assert_eq!(&result[16..], &tag);
+    let result = aes.decrypt(&result[..], b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..], &msg);
+}
+
+/// Test AES-256-GCM encryption/decryption
+pub fn aes_256_gcm_test_tc79_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC79
+    let test_key = hex!("59d4eafb4de0cfc7d3db99a8f54b15d7b39f0acc8da69763b019c1699f87674a");
+    let nonce = hex!("2fcb1b38a99e71b84740ad9b");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("549b365af913f3b081131ccb6b825588");
+    let ct = hex!("f58c16690122d75356907fd96b570fca");
+    let tag = hex!("28752c20153092818faba2a334640d6e");
+    let mut buf = Vec::from(msg.as_slice());
+    let actual_tag = aes.encrypt_detached(&mut buf, b"", &nonce).unwrap();
+    assert_eq!(&buf, &ct);
+    assert_eq!(&actual_tag, &tag);
+    assert!(aes.decrypt_detached(&mut buf, b"", &nonce, &tag).is_ok());
+    assert_eq!(&buf, &msg);
+}
+
+/// Test AES-256-GCM encryption/decryption where the tag given to decryption doesn't match.
+pub fn aes_256_gcm_test_decrypt_detached_bad_tag<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcm<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_test.json
+    // TC94
+    let test_key = hex!("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
+    let nonce = hex!("505152535455565758595a5b");
+    let aes = A::new(&test_key.into());
+    let aad = hex!("");
+    let ct = hex!("b2061457c0759fc1749f174ee1ccadfa");
+    let bad_tag = hex!("9de8fef6d8ab1bf1bf887232eab590dd");
+    let mut buf = Vec::from(ct.as_slice());
+    aes.decrypt_detached(&mut buf, &aad, &nonce, &bad_tag)
+        .expect_err("Decrypting with bad tag should fail");
+    // assert that the buffer does not change if tag verification fails
+    assert_eq!(buf, ct);
+}
+
+/// Generates the test cases to validate the AES-128-GCM implementation.
+/// For example, to test `MyAesGcm128Impl`:
+///
+/// ```
+/// use crypto_provider::aes::aes_gcm::testing::*;
+///
+/// mod tests {
+///     #[apply(aes_128_gcm_test_cases)]
+///     fn aes_128_gcm_tests(testcase: CryptoProviderTestCase<MyAesGcmImpl>) {
+///         testcase(MyAesGcm128Impl);
+///     }
+/// }
+/// ```
+#[template]
+#[export]
+#[rstest]
+#[case::encrypt(aes_128_gcm_test_encrypt)]
+#[case::decrypt(aes_128_gcm_test_decrypt)]
+fn aes_128_gcm_test_cases<F: AesGcmFactory<Key = Aes128Key>>(
+    #[case] testcase: CryptoProviderTestCase<F>,
+) {
+}
+
+/// Generates the test cases to validate the AES-128-GCM implementation.
+/// For example, to test `MyAesGcm128Impl`:
+///
+/// ```
+/// use crypto_provider::aes::aes_gcm::testing::*;
+///
+/// mod tests {
+///     #[apply(aes_128_gcm_test_cases_detached)]
+///     fn aes_128_gcm_tests(testcase: CryptoProviderTestCase<MyAesGcmImpl>) {
+///         testcase(MyAesGcm128Impl);
+///     }
+/// }
+/// ```
+#[template]
+#[export]
+#[rstest]
+#[case::encrypt_detached(aes_128_gcm_test_encrypt_detached)]
+#[case::decrypt_detached(aes_128_gcm_test_decrypt_detached)]
+#[case::decrypt_detached_bad_tag(aes_128_gcm_test_decrypt_detached_bad_tag)]
+fn aes_128_gcm_test_cases_detached<F: AesGcmFactory<Key = Aes128Key>>(
+    #[case] testcase: CryptoProviderTestCase<F>,
+) {
+}
+
+/// Generates the test cases to validate the AES-256-GCM implementation.
+/// For example, to test `MyAesGcm256Impl`:
+///
+/// ```
+/// use crypto_provider::aes::aes_gcm::testing::*;
+///
+/// mod tests {
+///     #[apply(aes_256_gcm_test_cases)]
+///     fn aes_256_gcm_tests(testcase: CryptoProviderTestCase<MyAesGcm256Impl>) {
+///         testcase(MyAesGcm256Impl);
+///     }
+/// }
+/// ```
+#[template]
+#[export]
+#[rstest]
+#[case::tc74(aes_256_gcm_test_tc74)]
+#[case::tc79(aes_256_gcm_test_tc79)]
+fn aes_256_gcm_test_cases<F: AesGcmFactory<Key = Aes256Key>>(
+    #[case] testcase: CryptoProviderTestCase<F>,
+) {
+}
+
+/// Generates the test cases to validate the AES-256-GCM implementation.
+/// For example, to test `MyAesGcm256Impl`:
+///
+/// ```
+/// use crypto_provider::aes::aes_gcm::testing::*;
+///
+/// mod tests {
+///     #[apply(aes_256_gcm_test_cases_detached)]
+///     fn aes_256_gcm_tests(testcase: CryptoProviderTestCase<MyAesGcm256Impl>) {
+///         testcase(MyAesGcm256Impl);
+///     }
+/// }
+/// ```
+#[template]
+#[export]
+#[rstest]
+#[case::tc74_detached(aes_256_gcm_test_tc74_detached)]
+#[case::tc79_detached(aes_256_gcm_test_tc79_detached)]
+#[case::decrypt_detached_bad_tag(aes_256_gcm_test_decrypt_detached_bad_tag)]
+fn aes_256_gcm_test_cases_detached<F: AesGcmFactory<Key = Aes256Key>>(
+    #[case] testcase: CryptoProviderTestCase<F>,
+) {
+}
diff --git a/nearby/crypto/crypto_provider_test/src/aead/aes_gcm_siv.rs b/nearby/crypto/crypto_provider_test/src/aead/aes_gcm_siv.rs
index 893fba2..56d2215 100644
--- a/nearby/crypto/crypto_provider_test/src/aead/aes_gcm_siv.rs
+++ b/nearby/crypto/crypto_provider_test/src/aead/aes_gcm_siv.rs
@@ -18,12 +18,28 @@
 use rstest_reuse::template;
 
 pub use crate::prelude;
-use crypto_provider::aes::{Aes128Key, Aes256Key};
+use crypto_provider::aead::{AeadInit, AesGcmSiv};
 
-use crypto_provider::aead::aes_gcm_siv::AesGcmSiv;
+/// Test AES-GCM-SIV-128 encryption
+pub fn aes_128_gcm_siv_test_encrypt<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
+    // TC1
+    let test_key = hex!("01000000000000000000000000000000");
+    let nonce = hex!("030000000000000000000000");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("");
+    let tag = hex!("dc20e2d83f25705bb49e439eca56de25");
+    let result = aes.encrypt(&msg, b"", &nonce).expect("Should succeed");
+    assert_eq!(&result[..], &tag);
+}
 
-/// Test AES-GCM-SIV-128 encryption/decryption
-pub fn aes_128_gcm_siv_test<A: AesGcmSiv<Key = Aes128Key>>(_marker: marker::PhantomData<A>) {
+pub fn aes_128_gcm_siv_test_encrypt_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
     // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
     // TC1
     let test_key = hex!("01000000000000000000000000000000");
@@ -32,23 +48,102 @@
     let msg = hex!("");
     let mut buf = Vec::from(msg.as_slice());
     let tag = hex!("dc20e2d83f25705bb49e439eca56de25");
-    assert!(aes.encrypt(&mut buf, b"", &nonce).is_ok());
-    assert_eq!(&buf[..], &tag);
+    let actual_tag: [u8; 16] = aes.encrypt_detached(&mut buf, b"", &nonce).unwrap();
+    assert_eq!(&buf, &[0_u8; 0]);
+    assert_eq!(&actual_tag, &tag);
+}
+
+/// Test AES-GCM-SIV-128 decryption
+pub fn aes_128_gcm_siv_test_decrypt<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
     // TC2
+    let test_key = hex!("01000000000000000000000000000000");
+    let nonce = hex!("030000000000000000000000");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("0100000000000000");
+    let ct = hex!("b5d839330ac7b786");
+    let tag = hex!("578782fff6013b815b287c22493a364c");
+    let result = aes.encrypt(&msg, b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..8], &ct);
+    assert_eq!(&result[8..], &tag);
+    assert_eq!(A::TAG_SIZE, result[8..].len());
+    let result = aes.decrypt(&result[..], b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..], &msg);
+}
+
+/// Test AES-GCM-SIV-128 decryption
+pub fn aes_128_gcm_siv_test_decrypt_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
+    // TC2
+    let test_key = hex!("01000000000000000000000000000000");
+    let nonce = hex!("030000000000000000000000");
+    let aes = A::new(&test_key.into());
     let msg = hex!("0100000000000000");
     let ct = hex!("b5d839330ac7b786");
     let tag = hex!("578782fff6013b815b287c22493a364c");
     let mut buf = Vec::from(msg.as_slice());
-    assert!(aes.encrypt(&mut buf, b"", &nonce).is_ok());
-    assert_eq!(&buf[..8], &ct);
-    assert_eq!(&buf[8..], &tag);
-    assert_eq!(A::TAG_SIZE, buf[8..].len());
-    assert!(aes.decrypt(&mut buf, b"", &nonce).is_ok());
+    let actual_tag = aes.encrypt_detached(&mut buf, b"", &nonce).unwrap();
+    assert_eq!(&buf, &ct);
+    assert_eq!(actual_tag, tag);
+    assert!(aes.decrypt_detached(&mut buf, b"", &nonce, &tag).is_ok());
     assert_eq!(&buf[..], &msg);
 }
 
+/// Test AES-GCM-SIV-128 decryption where the tag given to decryption doesn't match
+pub fn aes_128_gcm_siv_test_decrypt_detached_bad_tag<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes128Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
+    // TC45
+    let test_key = hex!("00112233445566778899aabbccddeeff");
+    let nonce = hex!("000000000000000000000000");
+    let aad = hex!("9ea3371e258288d5a01b15384e2c99ee");
+    let aes = A::new(&test_key.into());
+    // Use a longer ciphertext as the test case to make sure it's not unchanged only for the most
+    // recent block.
+    let ct = hex!(
+        "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
+        "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
+    );
+    let bad_tag = hex!("13a1883272188b4c8d2727178198fe95");
+    let mut buf = Vec::from(ct.as_slice());
+    aes.decrypt_detached(&mut buf, &aad, &nonce, &bad_tag).expect_err("Decryption should fail");
+    assert_eq!(&buf, &ct); // Buffer should be unchanged if decryption failed
+}
+
 /// Test AES-256-GCM-SIV encryption/decryption
-pub fn aes_256_gcm_siv_test<A: AesGcmSiv<Key = Aes256Key>>(_marker: marker::PhantomData<A>) {
+pub fn aes_256_gcm_siv_test_tc77<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
+    // TC77
+    let test_key = hex!("0100000000000000000000000000000000000000000000000000000000000000");
+    let nonce = hex!("030000000000000000000000");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("0100000000000000");
+    let ct = hex!("c2ef328e5c71c83b");
+    let tag = hex!("843122130f7364b761e0b97427e3df28");
+    let result = aes.encrypt(&msg, b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..8], &ct);
+    assert_eq!(&result[8..], &tag);
+    assert_eq!(A::TAG_SIZE, result[8..].len());
+    let result = aes.decrypt(&result[..], b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..], &msg);
+}
+
+/// Test AES-256-GCM-SIV encryption/decryption
+pub fn aes_256_gcm_siv_test_tc77_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
     // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
     // TC77
     let test_key = hex!("0100000000000000000000000000000000000000000000000000000000000000");
@@ -59,22 +154,72 @@
     buf.extend_from_slice(&msg);
     let ct = hex!("c2ef328e5c71c83b");
     let tag = hex!("843122130f7364b761e0b97427e3df28");
-    assert!(aes.encrypt(&mut buf, b"", &nonce).is_ok());
-    assert_eq!(&buf[..8], &ct);
-    assert_eq!(&buf[8..], &tag);
-    assert_eq!(A::TAG_SIZE, buf[8..].len());
-    assert!(aes.decrypt(&mut buf, b"", &nonce).is_ok());
-    assert_eq!(&buf[..], &msg);
+    let actual_tag = aes.encrypt_detached(&mut buf, b"", &nonce).unwrap();
+    assert_eq!(&buf, &ct);
+    assert_eq!(&actual_tag, &tag);
+    assert_eq!(A::TAG_SIZE, tag.len());
+    assert!(aes.decrypt_detached(&mut buf, b"", &nonce, &actual_tag).is_ok());
+    assert_eq!(&buf, &msg);
+}
+
+/// Test AES-256-GCM-SIV encryption/decryption
+pub fn aes_256_gcm_siv_test_tc78<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
     // TC78
+    let test_key = hex!("0100000000000000000000000000000000000000000000000000000000000000");
+    let nonce = hex!("030000000000000000000000");
+    let aes = A::new(&test_key.into());
+    let msg = hex!("010000000000000000000000");
+    let ct = hex!("9aab2aeb3faa0a34aea8e2b1");
+    let tag = hex!("8ca50da9ae6559e48fd10f6e5c9ca17e");
+    let result = aes.encrypt(&msg, b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..12], &ct);
+    assert_eq!(&result[12..], &tag);
+    let result = aes.decrypt(&result[..], b"", &nonce).expect("should succeed");
+    assert_eq!(&result[..], &msg);
+}
+
+/// Test AES-256-GCM-SIV encryption/decryption
+pub fn aes_256_gcm_siv_test_tc78_detached<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
+    // TC78
+    let test_key = hex!("0100000000000000000000000000000000000000000000000000000000000000");
+    let nonce = hex!("030000000000000000000000");
+    let aes = A::new(&test_key.into());
     let msg = hex!("010000000000000000000000");
     let ct = hex!("9aab2aeb3faa0a34aea8e2b1");
     let tag = hex!("8ca50da9ae6559e48fd10f6e5c9ca17e");
     let mut buf = Vec::from(msg.as_slice());
-    assert!(aes.encrypt(&mut buf, b"", &nonce).is_ok());
-    assert_eq!(&buf[..12], &ct);
-    assert_eq!(&buf[12..], &tag);
-    assert!(aes.decrypt(&mut buf, b"", &nonce).is_ok());
-    assert_eq!(&buf[..], &msg);
+    let actual_tag = aes.encrypt_detached(&mut buf, b"", &nonce).unwrap();
+    assert_eq!(&buf, &ct);
+    assert_eq!(&actual_tag, &tag);
+    assert!(aes.decrypt_detached(&mut buf, b"", &nonce, &tag).is_ok());
+    assert_eq!(&buf, &msg);
+}
+
+/// Test AES-256-GCM-SIV encryption/decryption where the tag given to decryption doesn't match.
+pub fn aes_256_gcm_siv_test_decrypt_detached_bad_tag<A>(_marker: marker::PhantomData<A>)
+where
+    A: AesGcmSiv<Tag = [u8; 16]> + AeadInit<crypto_provider::aes::Aes256Key>,
+{
+    // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json
+    // TC122
+    let test_key = hex!("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff");
+    let nonce = hex!("000000000000000000000000");
+    let aes = A::new(&test_key.into());
+    let aad = hex!("0289eaa93eb084107d2088435ef2a0cd");
+    let ct = hex!("ffffffffffffffff");
+    let bad_tag = hex!("ffffffffffffffffffffffffffffffff");
+    let mut buf = Vec::from(ct.as_slice());
+    aes.decrypt_detached(&mut buf, &aad, &nonce, &bad_tag)
+        .expect_err("Decrypting with bad tag should fail");
+    assert_eq!(&buf, &ct); // The buffer should be unchanged if the decryption failed
 }
 
 /// Generates the test cases to validate the AES-128-GCM-SIV implementation.
@@ -93,13 +238,37 @@
 #[template]
 #[export]
 #[rstest]
-#[case::encrypt(aes_128_gcm_siv_test)]
-#[case::decrypt(aes_128_gcm_siv_test)]
+#[case::encrypt(aes_128_gcm_siv_test_encrypt)]
+#[case::decrypt(aes_128_gcm_siv_test_decrypt)]
 fn aes_128_gcm_siv_test_cases<F: AesGcmSivFactory<Key = Aes128Key>>(
     #[case] testcase: CryptoProviderTestCase<F>,
 ) {
 }
 
+/// Generates the test cases to validate the AES-128-GCM-SIV implementation.
+/// For example, to test `MyAesGcmSiv128Impl`:
+///
+/// ```
+/// use crypto_provider::aes::aes_gcm_siv::testing::*;
+///
+/// mod tests {
+///     #[apply(aes_128_gcm_siv_test_cases_detached)]
+///     fn aes_128_gcm_siv_tests(testcase: CryptoProviderTestCase<MyAesGcmSivImpl>) {
+///         testcase(MyAesGcmSiv128Impl);
+///     }
+/// }
+/// ```
+#[template]
+#[export]
+#[rstest]
+#[case::encrypt_detached(aes_128_gcm_siv_test_encrypt_detached)]
+#[case::decrypt_detached(aes_128_gcm_siv_test_decrypt_detached)]
+#[case::decrypt_detached_bad_tag(aes_128_gcm_siv_test_decrypt_detached_bad_tag)]
+fn aes_128_gcm_siv_test_cases_detached<F: AesGcmSivFactory<Key = Aes128Key>>(
+    #[case] testcase: CryptoProviderTestCase<F>,
+) {
+}
+
 /// Generates the test cases to validate the AES-256-GCM-SIV implementation.
 /// For example, to test `MyAesGcmSiv256Impl`:
 ///
@@ -116,9 +285,33 @@
 #[template]
 #[export]
 #[rstest]
-#[case::encrypt(aes_256_gcm_siv_test)]
-#[case::decrypt(aes_256_gcm_siv_test)]
+#[case::tc77(aes_256_gcm_siv_test_tc77)]
+#[case::tc78(aes_256_gcm_siv_test_tc78)]
 fn aes_256_gcm_siv_test_cases<F: AesGcmSivFactory<Key = Aes256Key>>(
     #[case] testcase: CryptoProviderTestCase<F>,
 ) {
 }
+
+/// Generates the test cases to validate the AES-256-GCM-SIV implementation.
+/// For example, to test `MyAesGcmSiv256Impl`:
+///
+/// ```
+/// use crypto_provider::aes::aes_gcm_siv::testing::*;
+///
+/// mod tests {
+///     #[apply(aes_256_gcm_siv_test_cases_detached)]
+///     fn aes_256_gcm_siv_tests(testcase: CryptoProviderTestCase<MyAesGcmSiv256Impl>) {
+///         testcase(MyAesGcmSiv256Impl);
+///     }
+/// }
+/// ```
+#[template]
+#[export]
+#[rstest]
+#[case::tc77_detached(aes_256_gcm_siv_test_tc77_detached)]
+#[case::tc78_detached(aes_256_gcm_siv_test_tc78_detached)]
+#[case::decrypt_detached_bad_tag(aes_256_gcm_siv_test_decrypt_detached_bad_tag)]
+fn aes_256_gcm_siv_test_cases_detached<F: AesGcmSivFactory<Key = Aes256Key>>(
+    #[case] testcase: CryptoProviderTestCase<F>,
+) {
+}
diff --git a/nearby/crypto/crypto_provider_test/src/aead/mod.rs b/nearby/crypto/crypto_provider_test/src/aead/mod.rs
index 962aa49..3fe857c 100644
--- a/nearby/crypto/crypto_provider_test/src/aead/mod.rs
+++ b/nearby/crypto/crypto_provider_test/src/aead/mod.rs
@@ -14,3 +14,6 @@
 
 /// Contains test cases for aes_gcm_siv implementations.
 pub mod aes_gcm_siv;
+
+/// Contains test cases for aes_gcm implementations.
+pub mod aes_gcm;
diff --git a/nearby/crypto/crypto_provider_test/src/aes/cbc.rs b/nearby/crypto/crypto_provider_test/src/aes/cbc.rs
index b22c828..46cbebe 100644
--- a/nearby/crypto/crypto_provider_test/src/aes/cbc.rs
+++ b/nearby/crypto/crypto_provider_test/src/aes/cbc.rs
@@ -15,13 +15,17 @@
 use crate::aes::Aes256Key;
 pub use crate::prelude::*;
 use core::marker::PhantomData;
-use crypto_provider::aes::cbc::{AesCbcIv, AesCbcPkcs7Padded};
+use crypto_provider::{
+    aes::cbc::{AesCbcIv, AesCbcPkcs7Padded},
+    tinyvec::SliceVec,
+};
 use hex_literal::hex;
 use rstest_reuse::template;
 
 /// Tests for AES-256-CBC encryption
 pub fn aes_256_cbc_test_encrypt<A: AesCbcPkcs7Padded>(_marker: PhantomData<A>) {
-    // http://google3/third_party/wycheproof/testvectors/aes_cbc_pkcs5_test.json;l=1492;rcl=264817632
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1492
+    // tcId: 132
     let key: Aes256Key =
         hex!("665a02bc265a66d01775091da56726b6668bfd903cb7af66fb1b78a8a062e43c").into();
     let iv: AesCbcIv = hex!("3fb0d5ecd06c71150748b599595833cb");
@@ -30,9 +34,47 @@
     assert_eq!(A::encrypt(&key, &iv, &msg), expected_ciphertext);
 }
 
+/// Tests for AES-256-CBC in-place encryption
+pub fn aes_256_cbc_test_encrypt_in_place<A: AesCbcPkcs7Padded>(_marker: PhantomData<A>) {
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1492
+    // tcId: 132
+    let key: Aes256Key =
+        hex!("665a02bc265a66d01775091da56726b6668bfd903cb7af66fb1b78a8a062e43c").into();
+    let iv: AesCbcIv = hex!("3fb0d5ecd06c71150748b599595833cb");
+    let msg = hex!("3f56935def3f");
+    let expected_ciphertext = hex!("3f3f39697bd7e88d85a14132be1cbc48");
+    let mut msg_buffer_backing = [0_u8; 16];
+    let mut msg_buffer = SliceVec::from_slice_len(&mut msg_buffer_backing, 0);
+    msg_buffer.extend_from_slice(&msg);
+    A::encrypt_in_place(&key, &iv, &mut msg_buffer).unwrap();
+    assert_eq!(msg_buffer.as_slice(), &expected_ciphertext);
+}
+
+/// Tests for AES-256-CBC encryption, where the given buffer `SliceVec` is too short to contain the
+/// output.
+pub fn aes_256_cbc_test_encrypt_in_place_too_short<A: AesCbcPkcs7Padded>(_marker: PhantomData<A>) {
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1612
+    // tcId: 144
+    let key: Aes256Key =
+        hex!("4f097858a1aec62cf18f0966b2b120783aa4ae9149d3213109740506ae47adfe").into();
+    let iv: AesCbcIv = hex!("400aab92803bcbb44a96ef789655b34e");
+    let msg = hex!("ee53d8e5039e82d9fcca114e375a014febfea117a7e709d9008d43858e3660");
+    let mut msg_buffer_backing = [0_u8; 31];
+    let mut msg_buffer = SliceVec::from_slice_len(&mut msg_buffer_backing, 0);
+    msg_buffer.extend_from_slice(&msg);
+    A::encrypt_in_place(&key, &iv, &mut msg_buffer)
+        .expect_err("Encrypting AES with 15-byte buffer should fail");
+    // Buffer content is undefined, but test to make sure it doesn't contain half-decrypted data
+    assert!(
+        msg_buffer.as_slice() == [0_u8; 32] || msg_buffer.as_slice() == msg,
+        "Unrecognized content in buffer after decryption failure"
+    )
+}
+
 /// Tests for AES-256-CBC decryption
 pub fn aes_256_cbc_test_decrypt<A: AesCbcPkcs7Padded>(_marker: PhantomData<A>) {
-    // http://google3/third_party/wycheproof/testvectors/aes_cbc_pkcs5_test.json;l=1492;rcl=264817632
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1492
+    // tcId: 132
     let key: Aes256Key =
         hex!("665a02bc265a66d01775091da56726b6668bfd903cb7af66fb1b78a8a062e43c").into();
     let iv: AesCbcIv = hex!("3fb0d5ecd06c71150748b599595833cb");
@@ -41,6 +83,52 @@
     assert_eq!(A::decrypt(&key, &iv, &ciphertext).unwrap(), expected_msg);
 }
 
+/// Tests for AES-256-CBC decryption with bad padding
+pub fn aes_256_cbc_test_decrypt_bad_padding<A: AesCbcPkcs7Padded>(_marker: PhantomData<A>) {
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1690
+    // tcId: 151
+    let key: Aes256Key =
+        hex!("7c78f34dbce8f0557d43630266f59babd1cb92ba624bd1a8f45a2a91c84a804a").into();
+    let iv: AesCbcIv = hex!("f010f61c31c9aa8fa0d5be5f6b0f2f70");
+    let ciphertext = hex!("8881e9e02fa9e3037b397957ba1fb7ce64679a46621b792f643542a735f0bbbf");
+    A::decrypt(&key, &iv, &ciphertext).expect_err("Decryption with bad padding should fail");
+}
+
+/// Tests for AES-256-CBC in-place decryption
+pub fn aes_256_cbc_test_decrypt_in_place<A: AesCbcPkcs7Padded>(_marker: PhantomData<A>) {
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1492
+    // tcId: 132
+    let key: Aes256Key =
+        hex!("665a02bc265a66d01775091da56726b6668bfd903cb7af66fb1b78a8a062e43c").into();
+    let iv: AesCbcIv = hex!("3fb0d5ecd06c71150748b599595833cb");
+    let mut ciphertext = hex!("3f3f39697bd7e88d85a14132be1cbc48");
+    let expected_msg = hex!("3f56935def3f");
+    let mut msg_buffer = SliceVec::from(&mut ciphertext);
+    A::decrypt_in_place(&key, &iv, &mut msg_buffer).unwrap();
+    assert_eq!(msg_buffer.as_slice(), expected_msg);
+}
+
+/// Tests for AES-256-CBC in-place decryption with bad padding
+pub fn aes_256_cbc_test_decrypt_in_place_bad_padding<A: AesCbcPkcs7Padded>(
+    _marker: PhantomData<A>,
+) {
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/aes_cbc_pkcs5_test.json#L1690
+    // tcId: 151
+    let key: Aes256Key =
+        hex!("7c78f34dbce8f0557d43630266f59babd1cb92ba624bd1a8f45a2a91c84a804a").into();
+    let iv: AesCbcIv = hex!("f010f61c31c9aa8fa0d5be5f6b0f2f70");
+    let ciphertext = hex!("8881e9e02fa9e3037b397957ba1fb7ce64679a46621b792f643542a735f0bbbf");
+    let mut msg_buffer_backing = ciphertext;
+    let mut msg_buffer = SliceVec::from(&mut msg_buffer_backing);
+    A::decrypt_in_place(&key, &iv, &mut msg_buffer)
+        .expect_err("Decryption with bad padding should fail");
+    // Buffer content is undefined, but test to make sure it doesn't contain half-decrypted data
+    assert!(
+        msg_buffer.as_slice() == [0_u8; 32] || msg_buffer.as_slice() == ciphertext,
+        "Unrecognized content in buffer after decryption failure"
+    )
+}
+
 /// Generates the test cases to validate the AES-256-CBC implementation.
 /// For example, to test `MyAesCbc256Impl`:
 ///
@@ -59,5 +147,9 @@
 #[export]
 #[rstest]
 #[case::encrypt(aes_256_cbc_test_encrypt)]
+#[case::encrypt_in_place(aes_256_cbc_test_encrypt_in_place)]
+#[case::encrypt_in_place_too_short(aes_256_cbc_test_encrypt_in_place_too_short)]
 #[case::decrypt(aes_256_cbc_test_decrypt)]
+#[case::decrypt_bad_padding(aes_256_cbc_test_decrypt_bad_padding)]
+#[case::decrypt_in_place_bad_padding(aes_256_cbc_test_decrypt_in_place_bad_padding)]
 fn aes_256_cbc_test_cases<A: AesCbcPkcs7Padded>(#[case] testcase: CryptoProviderTestCases<F>) {}
diff --git a/nearby/crypto/crypto_provider_test/src/aes/ctr.rs b/nearby/crypto/crypto_provider_test/src/aes/ctr.rs
index b1d6b8b..de71740 100644
--- a/nearby/crypto/crypto_provider_test/src/aes/ctr.rs
+++ b/nearby/crypto/crypto_provider_test/src/aes/ctr.rs
@@ -27,22 +27,22 @@
     let mut cipher = A::new(&key, NonceAndCounter::from_block(iv));
 
     block = hex!("6bc1bee22e409f96e93d7e117393172a");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_1 = hex!("874d6191b620e3261bef6864990db6ce");
     assert_eq!(expected_ciphertext_1, block);
 
     block = hex!("ae2d8a571e03ac9c9eb76fac45af8e51");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_2 = hex!("9806f66b7970fdff8617187bb9fffdff");
     assert_eq!(expected_ciphertext_2, block);
 
     block = hex!("30c81c46a35ce411e5fbc1191a0a52ef");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_3 = hex!("5ae4df3edbd5d35e5b4f09020db03eab");
     assert_eq!(expected_ciphertext_3, block);
 
     block = hex!("f69f2445df4f9b17ad2b417be66c3710");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_3 = hex!("1e031dda2fbe03d1792170a0f3009cee");
     assert_eq!(expected_ciphertext_3, block);
 }
@@ -56,22 +56,22 @@
     let mut cipher = A::new(&key, NonceAndCounter::from_block(iv));
 
     block = hex!("874d6191b620e3261bef6864990db6ce");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_1 = hex!("6bc1bee22e409f96e93d7e117393172a");
     assert_eq!(expected_plaintext_1, block);
 
     block = hex!("9806f66b7970fdff8617187bb9fffdff");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_2 = hex!("ae2d8a571e03ac9c9eb76fac45af8e51");
     assert_eq!(expected_plaintext_2, block);
 
     block = hex!("5ae4df3edbd5d35e5b4f09020db03eab");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_3 = hex!("30c81c46a35ce411e5fbc1191a0a52ef");
     assert_eq!(expected_plaintext_3, block);
 
     block = hex!("1e031dda2fbe03d1792170a0f3009cee");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_3 = hex!("f69f2445df4f9b17ad2b417be66c3710");
     assert_eq!(expected_plaintext_3, block);
 }
@@ -86,22 +86,22 @@
     let mut cipher = A::new(&key, NonceAndCounter::from_block(iv));
 
     block = hex!("6bc1bee22e409f96e93d7e117393172a");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_1 = hex!("601ec313775789a5b7a7f504bbf3d228");
     assert_eq!(expected_ciphertext_1, block);
 
     block = hex!("ae2d8a571e03ac9c9eb76fac45af8e51");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_2 = hex!("f443e3ca4d62b59aca84e990cacaf5c5");
     assert_eq!(expected_ciphertext_2, block);
 
     block = hex!("30c81c46a35ce411e5fbc1191a0a52ef");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_3 = hex!("2b0930daa23de94ce87017ba2d84988d");
     assert_eq!(expected_ciphertext_3, block);
 
     block = hex!("f69f2445df4f9b17ad2b417be66c3710");
-    cipher.encrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_ciphertext_3 = hex!("dfc9c58db67aada613c2dd08457941a6");
     assert_eq!(expected_ciphertext_3, block);
 }
@@ -116,22 +116,22 @@
     let mut cipher = A::new(&key, NonceAndCounter::from_block(iv));
 
     block = hex!("601ec313775789a5b7a7f504bbf3d228");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_1 = hex!("6bc1bee22e409f96e93d7e117393172a");
     assert_eq!(expected_plaintext_1, block);
 
     block = hex!("f443e3ca4d62b59aca84e990cacaf5c5");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_2 = hex!("ae2d8a571e03ac9c9eb76fac45af8e51");
     assert_eq!(expected_plaintext_2, block);
 
     block = hex!("2b0930daa23de94ce87017ba2d84988d");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_3 = hex!("30c81c46a35ce411e5fbc1191a0a52ef");
     assert_eq!(expected_plaintext_3, block);
 
     block = hex!("dfc9c58db67aada613c2dd08457941a6");
-    cipher.decrypt(&mut block);
+    cipher.apply_keystream(&mut block);
     let expected_plaintext_3 = hex!("f69f2445df4f9b17ad2b417be66c3710");
     assert_eq!(expected_plaintext_3, block);
 }
diff --git a/nearby/crypto/crypto_provider_test/src/ed25519.rs b/nearby/crypto/crypto_provider_test/src/ed25519.rs
index d99605c..0fdb484 100644
--- a/nearby/crypto/crypto_provider_test/src/ed25519.rs
+++ b/nearby/crypto/crypto_provider_test/src/ed25519.rs
@@ -18,7 +18,9 @@
 use alloc::borrow::ToOwned;
 use alloc::string::String;
 use alloc::vec::Vec;
-use crypto_provider::ed25519::{Ed25519Provider, KeyPair, PublicKey, Signature};
+use crypto_provider::ed25519::{
+    Ed25519Provider, KeyPair, PublicKey, RawPrivateKeyPermit, RawSignature, Signature,
+};
 use wycheproof::TestResult;
 
 // These are test vectors from the creators of Ed25519: https://ed25519.cr.yp.to/ which are referenced
@@ -27,7 +29,7 @@
 // also used for test cases in the above RFC:
 // https://dev.gnupg.org/source/libgcrypt/browse/master/tests/t-ed25519.inp
 const PATH_TO_RFC_VECTORS_FILE: &str =
-    "crypto/crypto_provider_test/src/testdata/ecdsa/rfc_test_vectors.txt";
+    "crypto/crypto_provider_test/src/testdata/EdDSA/rfc_test_vectors.txt";
 
 /// Runs set of Ed25519 wycheproof test vectors against a provided ed25519 implementation
 /// Tests vectors from Project Wycheproof: <https://github.com/google/wycheproof>
@@ -39,10 +41,7 @@
         .expect("should be able to load test set");
 
     for test_group in test_set.test_groups {
-        let key_pair = test_group.key;
-        let public_key = key_pair.pk;
-        let secret_key = key_pair.sk;
-
+        let public_key = test_group.key.pk.to_vec();
         for test in test_group.tests {
             let tc_id = test.tc_id;
             let comment = test.comment;
@@ -53,8 +52,7 @@
                 TestResult::Invalid => false,
                 TestResult::Valid | TestResult::Acceptable => true,
             };
-            let result =
-                run_test::<E>(public_key.clone(), secret_key.clone(), sig.clone(), msg.clone());
+            let result = run_wycheproof_test::<E>(public_key.to_vec(), sig.to_vec(), msg.to_vec());
             if valid {
                 if let Err(desc) = result {
                     panic!(
@@ -73,6 +71,20 @@
     }
 }
 
+fn run_wycheproof_test<E>(pub_key: Vec<u8>, sig: Vec<u8>, msg: Vec<u8>) -> Result<(), &'static str>
+where
+    E: Ed25519Provider,
+{
+    let pub_key = E::PublicKey::from_bytes(pub_key.as_slice().try_into().unwrap())
+        .map_err(|_| "Invalid public key bytes")?;
+
+    let raw_sig: RawSignature =
+        sig.as_slice().try_into().map_err(|_| "Invalid length signature")?;
+    let signature = E::Signature::from_bytes(&raw_sig);
+
+    pub_key.verify_strict(msg.as_slice(), &signature).map_err(|_| "Signature verification failed")
+}
+
 /// Runs the RFC specified test vectors against an Ed25519 implementation
 pub fn run_rfc_test_vectors<E>()
 where
@@ -126,7 +138,10 @@
 {
     let private_key_bytes: [u8; 32] =
         private_key.as_slice().try_into().expect("Secret key is the wrong length");
-    let kp = E::KeyPair::from_private_key(&private_key_bytes);
+
+    // Permits: Test-only code, not a production leak of the private key
+    let permit = RawPrivateKeyPermit::default();
+    let kp = E::KeyPair::from_raw_private_key(&private_key_bytes, &permit);
 
     let sig_result = kp.sign(msg.as_slice());
     (sig.as_slice() == sig_result.to_bytes()).then_some(()).ok_or("sig not matching expected")?;
diff --git a/nearby/crypto/crypto_provider_test/src/lib.rs b/nearby/crypto/crypto_provider_test/src/lib.rs
index 8e63f44..2209a3e 100644
--- a/nearby/crypto/crypto_provider_test/src/lib.rs
+++ b/nearby/crypto/crypto_provider_test/src/lib.rs
@@ -36,9 +36,11 @@
 /// Common items that needs to be imported to use these test cases
 pub mod prelude {
     pub use super::CryptoProviderTestCase;
+    pub use ::rstest;
     pub use rstest::rstest;
     pub use rstest_reuse;
     pub use rstest_reuse::apply;
+    pub extern crate std;
 }
 
 /// A test case for Crypto Provider. A test case is a function that panics if the test fails.
diff --git a/nearby/crypto/crypto_provider_test/src/p256.rs b/nearby/crypto/crypto_provider_test/src/p256.rs
index 9869c9e..5ebbd50 100644
--- a/nearby/crypto/crypto_provider_test/src/p256.rs
+++ b/nearby/crypto/crypto_provider_test/src/p256.rs
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-extern crate std;
 use crate::elliptic_curve::EphemeralSecretForTesting;
 pub use crate::prelude::*;
 use crate::TestError;
 use core::marker::PhantomData;
-use crypto_provider::p256::{P256PublicKey, P256};
+use core::ops::Deref;
+use crypto_provider::p256::{P256PublicKey, PointCompression, P256};
 use crypto_provider::{
     elliptic_curve::{EcdhProvider, EphemeralSecret, PublicKey},
     CryptoRng,
@@ -62,9 +62,35 @@
              8bbe76c6dc1643088107636deff8aa79e8002a157b92"
     );
     let key = E::PublicKey::from_sec1_bytes(&sec1_bytes).unwrap();
+    // Not part of the API contract, but `to_bytes()` should prefer to use uncompressed
+    // representation since support for compressed point is optional.
+    let key_bytes = key.to_bytes();
+    assert_eq!(sec1_bytes.to_vec(), key_bytes.deref());
+}
+
+/// Test for P256PublicKey::to_sec1_bytes(Compressed). Support for compressed representation is
+/// optional.
+pub fn to_bytes_compressed_test<E: EcdhProviderForP256Test>(_: PhantomData<E>) {
+    let sec1_bytes = hex!(
+        "04756c07ba5b596fa96c9099e6619dc62deac4297a8fc1d803d74dc5caa9197c09f0b6da270d2a58a06022
+             8bbe76c6dc1643088107636deff8aa79e8002a157b92"
+    );
+    let key = E::PublicKey::from_sec1_bytes(&sec1_bytes).unwrap();
+    let key_bytes = key.to_sec1_bytes(PointCompression::Compressed);
     let sec1_bytes_compressed =
         hex!("02756c07ba5b596fa96c9099e6619dc62deac4297a8fc1d803d74dc5caa9197c09");
-    assert_eq!(sec1_bytes_compressed.to_vec(), key.to_bytes());
+    assert_eq!(sec1_bytes_compressed.to_vec(), key_bytes.deref());
+}
+
+/// Test for P256PublicKey::to_sec1_bytes(Uncompressed)
+pub fn to_bytes_uncompressed_test<E: EcdhProviderForP256Test>(_: PhantomData<E>) {
+    let sec1_bytes = hex!(
+        "04756c07ba5b596fa96c9099e6619dc62deac4297a8fc1d803d74dc5caa9197c09f0b6da270d2a58a06022
+             8bbe76c6dc1643088107636deff8aa79e8002a157b92"
+    );
+    let key = E::PublicKey::from_sec1_bytes(&sec1_bytes).unwrap();
+    let key_bytes = key.to_sec1_bytes(PointCompression::Uncompressed);
+    assert_eq!(sec1_bytes.to_vec(), key_bytes.deref());
 }
 
 /// Random test for P256PublicKey::to_bytes
@@ -75,7 +101,7 @@
                 P256,
             >>::Rng::new())
             .public_key_bytes();
-        let public_key = E::PublicKey::from_bytes(&public_key_bytes).unwrap();
+        let public_key = E::PublicKey::from_bytes(public_key_bytes.as_ref()).unwrap();
         assert_eq!(
             E::PublicKey::from_bytes(&public_key.to_bytes()).unwrap(),
             public_key,
@@ -192,7 +218,7 @@
 /// Test for P256 Diffie-Hellman key exchange.
 pub fn p256_ecdh_test<E: EcdhProviderForP256Test>(_: PhantomData<E>) {
     // From wycheproof ecdh_secp256r1_ecpoint_test.json, tcId 1
-    // http://google3/third_party/wycheproof/testvectors/ecdh_secp256r1_ecpoint_test.json;l=22;rcl=375894991
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/ecdh_secp256r1_ecpoint_test.json#L22
     // sec1 public key manually extracted from the ASN encoded test data
     let public_key_sec1 = hex!(
         "0462d5bd3372af75fe85a040715d0f502428e07046868b0bfdfa61d731afe44f
@@ -230,20 +256,24 @@
             };
             let result = p256_ecdh_test_impl::<E>(
                 &test.public_key,
-                &test.private_key.try_into().expect("Private key should be 32 bytes long"),
+                &test
+                    .private_key
+                    .as_slice()
+                    .try_into()
+                    .expect("Private key should be 32 bytes long"),
             );
             match test.result {
                 wycheproof::TestResult::Valid => {
                     let shared_secret =
                         result.unwrap_or_else(|_| panic!("Test {} should succeed", test.tc_id));
-                    assert_eq!(test.shared_secret, shared_secret.into());
+                    assert_eq!(test.shared_secret.as_slice(), shared_secret.into());
                 }
                 wycheproof::TestResult::Invalid => {
                     result.err().unwrap_or_else(|| panic!("Test {} should fail", test.tc_id));
                 }
                 wycheproof::TestResult::Acceptable => {
                     if let Ok(shared_secret) = result {
-                        assert_eq!(test.shared_secret, shared_secret.into());
+                        assert_eq!(test.shared_secret.as_slice(), shared_secret.into());
                     }
                     // Test passes if `result` is an error because this test is "acceptable"
                 }
@@ -268,22 +298,43 @@
 #[template]
 #[export]
 #[rstest]
-#[case::to_bytes(to_bytes_test)]
-#[case::to_bytes_random(to_bytes_random_test)]
-#[case::from_sec1_bytes_not_on_curve(from_sec1_bytes_not_on_curve_test)]
-#[case::from_sec1_bytes_not_on_infinity(from_sec1_bytes_at_infinity_test)]
-#[case::from_affine_coordinates(from_affine_coordinates_test)]
-#[case::from_affine_coordinates_not_on_curve(from_affine_coordinates_not_on_curve_test)]
-#[case::public_key_to_affine_coordinates(public_key_to_affine_coordinates_test)]
+#[case::to_bytes(to_bytes_test, "to_bytes")]
+#[case::to_bytes_compressed(to_bytes_compressed_test, "to_bytes_compressed")]
+#[case::to_bytes_uncompressed(to_bytes_uncompressed_test, "to_bytes_uncompressed")]
+#[case::to_bytes_random(to_bytes_random_test, "to_bytes_random")]
+#[case::from_sec1_bytes_not_on_curve(
+    from_sec1_bytes_not_on_curve_test,
+    "from_sec1_bytes_not_on_curve"
+)]
+#[case::from_sec1_bytes_not_on_infinity(
+    from_sec1_bytes_at_infinity_test,
+    "from_sec1_bytes_not_on_infinity"
+)]
+#[case::from_affine_coordinates(from_affine_coordinates_test, "from_affine_coordinates")]
+#[case::from_affine_coordinates_not_on_curve(
+    from_affine_coordinates_not_on_curve_test,
+    "from_affine_coordinates_not_on_curve"
+)]
+#[case::public_key_to_affine_coordinates(
+    public_key_to_affine_coordinates_test,
+    "public_key_to_affine_coordinates"
+)]
 #[case::public_key_to_affine_coordinates_compressed02(
-    public_key_to_affine_coordinates_compressed02_test
+    public_key_to_affine_coordinates_compressed02_test,
+    "public_key_to_affine_coordinates_compressed02"
 )]
 #[case::public_key_to_affine_coordinates_compressed03(
-    public_key_to_affine_coordinates_compressed03_test
+    public_key_to_affine_coordinates_compressed03_test,
+    "public_key_to_affine_coordinates_compressed03"
 )]
 #[case::public_key_to_affine_coordinates_zero_top_byte(
-    public_key_to_affine_coordinates_zero_top_byte_test
+    public_key_to_affine_coordinates_zero_top_byte_test,
+    "public_key_to_affine_coordinates_zero_top_byte"
 )]
-#[case::p256_ecdh(p256_ecdh_test)]
-#[case::wycheproof_p256(wycheproof_p256_test)]
-fn p256_test_cases<C: CryptoProvider>(#[case] testcase: CryptoProviderTestCase<C>) {}
+#[case::p256_ecdh(p256_ecdh_test, "p256_ecdh")]
+#[case::wycheproof_p256(wycheproof_p256_test, "wycheproof_p256")]
+fn p256_test_cases<C: CryptoProvider>(
+    #[case] testcase: CryptoProviderTestCase<C>,
+    #[case] name: &str,
+) {
+}
diff --git a/nearby/crypto/crypto_provider_test/src/sha2.rs b/nearby/crypto/crypto_provider_test/src/sha2.rs
index 330f284..66a3025 100644
--- a/nearby/crypto/crypto_provider_test/src/sha2.rs
+++ b/nearby/crypto/crypto_provider_test/src/sha2.rs
@@ -14,7 +14,7 @@
 
 extern crate alloc;
 extern crate std;
-pub use crate::prelude::*;
+use crate::prelude::*;
 use crate::CryptoProvider;
 use alloc::vec::Vec;
 use core::{marker::PhantomData, str::FromStr};
diff --git a/nearby/crypto/crypto_provider_test/src/testdata/ecdsa/rfc_test_vectors.txt b/nearby/crypto/crypto_provider_test/src/testdata/EdDSA/rfc_test_vectors.txt
similarity index 100%
rename from nearby/crypto/crypto_provider_test/src/testdata/ecdsa/rfc_test_vectors.txt
rename to nearby/crypto/crypto_provider_test/src/testdata/EdDSA/rfc_test_vectors.txt
diff --git a/nearby/crypto/crypto_provider_test/src/x25519.rs b/nearby/crypto/crypto_provider_test/src/x25519.rs
index 0fcaa12..1fe2019 100644
--- a/nearby/crypto/crypto_provider_test/src/x25519.rs
+++ b/nearby/crypto/crypto_provider_test/src/x25519.rs
@@ -58,7 +58,7 @@
 pub fn x25519_to_bytes_test<E: EcdhProviderForX25519Test>(_: PhantomData<E>) {
     let public_key_bytes = hex!("504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829");
     let public_key = E::PublicKey::from_bytes(&public_key_bytes).unwrap();
-    assert_eq!(public_key_bytes.to_vec(), public_key.to_bytes());
+    assert_eq!(public_key_bytes.to_vec(), public_key.to_bytes().as_ref());
 }
 
 /// Random test for `PublicKey<X25519>::to_bytes`
@@ -69,9 +69,9 @@
                 X25519,
             >>::Rng::new())
             .public_key_bytes();
-        let public_key = E::PublicKey::from_bytes(&public_key_bytes).unwrap();
+        let public_key = E::PublicKey::from_bytes(public_key_bytes.as_ref()).unwrap();
         assert_eq!(
-            E::PublicKey::from_bytes(&public_key.to_bytes()).unwrap(),
+            E::PublicKey::from_bytes(public_key.to_bytes().as_ref()).unwrap(),
             public_key,
             "from_bytes should return the same key for `{public_key_bytes:?}`",
         );
@@ -81,7 +81,7 @@
 /// Test for X25519 Diffie-Hellman key exchange.
 pub fn x25519_ecdh_test<E: EcdhProviderForX25519Test>(_: PhantomData<E>) {
     // From wycheproof ecdh_secx25519r1_ecpoint_test.json, tcId 1
-    // http://google3/third_party/wycheproof/testvectors/ecdh_secx25519r1_ecpoint_test.json;l=22;rcl=375894991
+    // https://github.com/google/wycheproof/blob/b063b4a/testvectors/x25519_test.json#L23
     // sec1 public key manually extracted from the ASN encoded test data
     let public_key = hex!("504a36999f489cd2fdbc08baff3d88fa00569ba986cba22548ffde80f9806829");
     let private = hex!("c8a9d5a91091ad851c668b0736c1c9a02936c0d3ad62670858088047ba057475");
@@ -109,20 +109,24 @@
         for test in test_group.tests {
             let result = x25519_ecdh_test_impl::<E>(
                 &test.public_key,
-                &test.private_key.try_into().expect("Private keys should be 32 bytes long"),
+                &test
+                    .private_key
+                    .as_slice()
+                    .try_into()
+                    .expect("Private keys should be 32 bytes long"),
             );
             match test.result {
                 wycheproof::TestResult::Valid => {
                     let shared_secret =
                         result.unwrap_or_else(|_| panic!("Test {} should succeed", test.tc_id));
-                    assert_eq!(&test.shared_secret, &shared_secret.into());
+                    assert_eq!(&test.shared_secret.as_slice(), &shared_secret.into());
                 }
                 wycheproof::TestResult::Invalid => {
                     result.err().unwrap_or_else(|| panic!("Test {} should fail", test.tc_id));
                 }
                 wycheproof::TestResult::Acceptable => {
                     if let Ok(shared_secret) = result {
-                        assert_eq!(test.shared_secret, shared_secret.into());
+                        assert_eq!(test.shared_secret.as_slice(), shared_secret.into());
                     }
                     // Test passes if `result` is an error because this test is "acceptable"
                 }
diff --git a/nearby/presence/CMakeLists.txt b/nearby/presence/CMakeLists.txt
index 2b4b66e..d2a251b 100644
--- a/nearby/presence/CMakeLists.txt
+++ b/nearby/presence/CMakeLists.txt
@@ -32,15 +32,6 @@
     set(CMAKE_CXX_FLAGS_DEBUG "-g ${CMAKE_C_FLAGS_DEBUG}")
 endif ()
 
-if (UNIX)
-    add_compile_options(-Wall -Wextra -Wimplicit-fallthrough -Wextra-semi
-            -Wno-missing-field-initializers -Wno-unused-parameter -Wno-psabi
-            -Wshadow
-            -Wsign-compare)
-elseif (MSVC)
-    add_compile_options(-W4 -O1)
-endif ()
-
 find_package(OpenSSL REQUIRED)
 if (OPENSSL_FOUND)
     message(STATUS "OpenSSL Found: ${OPENSSL_VERSION}")
@@ -48,6 +39,10 @@
     message(STATUS "OpenSSL Libraries: ${OPENSSL_LIBRARIES}")
 endif ()
 
+if (MSVC)
+    add_compile_options(-W4 -O1 -MD)
+endif ()
+
 if (ENABLE_TESTS)
     message(STATUS "Enabling workspace wide tests")
 
@@ -62,8 +57,8 @@
     enable_testing()
     include(GoogleTest)
 
-    # Find GoogleBenchmark
-    find_package(benchmark REQUIRED)
+    # Include google benchmark
+    add_subdirectory(${THIRD_PARTY_DIR}/benchmark ${THIRD_PARTY_DIR}/benchmark/build)
 
     # Setup jsoncpp
     set(JSONCPP_DIR ${THIRD_PARTY_DIR}/jsoncpp)
@@ -74,6 +69,15 @@
     )
 endif ()
 
+# Add these compile options for our own code, not needed for the above deps
+if (UNIX)
+    add_compile_options(-Wall -Wextra -Wimplicit-fallthrough -Wextra-semi
+            -Wno-missing-field-initializers -Wno-unused-parameter -Wno-psabi
+            -Wshadow
+            -Wsign-compare)
+endif()
+
+
 add_subdirectory(np_cpp_ffi)
 add_subdirectory(ldt_np_c_sample)
 
diff --git a/nearby/presence/array_ref/README.md b/nearby/presence/array_ref/README.md
new file mode 100644
index 0000000..2ff0748
--- /dev/null
+++ b/nearby/presence/array_ref/README.md
@@ -0,0 +1,4 @@
+This crate was originally forked from crates.io: https://github.com/droundy/arrayref
+
+This fork removes unsafe code which is no longer needed in stable rust while
+keeping the behavior the same. The owner of the upstream repo has been unresponsive with regards to landing these changes upstream ie: https://github.com/droundy/arrayref/pull/24
\ No newline at end of file
diff --git a/nearby/presence/handle_map/src/lib.rs b/nearby/presence/handle_map/src/lib.rs
deleted file mode 100644
index 745aec4..0000000
--- a/nearby/presence/handle_map/src/lib.rs
+++ /dev/null
@@ -1,608 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//! A thread-safe implementation of a map for managing object handles,
-//! a safer alternative to raw pointers for FFI interop.
-#![cfg_attr(not(test), no_std)]
-#![forbid(unsafe_code)]
-#![deny(missing_docs)]
-extern crate alloc;
-extern crate core;
-
-use alloc::boxed::Box;
-use alloc::vec::Vec;
-use core::ops::{Deref, DerefMut};
-use core::sync::atomic::Ordering;
-use hashbrown::hash_map::EntryRef;
-use portable_atomic::{AtomicU32, AtomicU64};
-use spin::lock_api::*;
-
-#[cfg(test)]
-mod tests;
-
-/// A RAII read lock guard for an object in a [`HandleMap`]
-/// pointed-to by a given [`Handle`]. When this struct is
-/// dropped, the underlying read lock on the associated
-/// shard will be dropped.
-pub struct ObjectReadGuard<'a, T> {
-    guard: lock_api::MappedRwLockReadGuard<'a, spin::RwLock<()>, T>,
-}
-
-impl<'a, T> Deref for ObjectReadGuard<'a, T> {
-    type Target = T;
-
-    fn deref(&self) -> &Self::Target {
-        self.guard.deref()
-    }
-}
-
-/// A RAII read-write lock guard for an object in a [`HandleMap`]
-/// pointed-to by a given [`Handle`]. When this struct is
-/// dropped, the underlying read-write lock on the associated
-/// shard will be dropped.
-pub struct ObjectReadWriteGuard<'a, T> {
-    guard: lock_api::MappedRwLockWriteGuard<'a, spin::RwLock<()>, T>,
-}
-
-impl<'a, T> Deref for ObjectReadWriteGuard<'a, T> {
-    type Target = T;
-
-    fn deref(&self) -> &Self::Target {
-        self.guard.deref()
-    }
-}
-
-impl<'a, T> DerefMut for ObjectReadWriteGuard<'a, T> {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        self.guard.deref_mut()
-    }
-}
-
-/// FFI-transmissible structure expressing the dimensions
-/// (max # of allocatable slots, number of shards) of a handle-map
-/// to be used upon initialization.
-#[repr(C)]
-#[derive(Clone, Copy)]
-pub struct HandleMapDimensions {
-    /// The number of shards which are employed
-    /// by the associated handle-map.
-    pub num_shards: u8,
-    /// The maximum number of active handles which may be
-    /// stored within the associated handle-map.
-    pub max_active_handles: u32,
-}
-
-/// Trait for marker structs containing information about a
-/// given [`SingletonHandleMap`], including the underlying
-/// object type and the dimensions of the map.
-/// Used primarily to enforce type-level constraints
-/// on `SingletoneHandleMap`s, including
-/// ensuring that they are truly singletons.
-pub trait SingletonHandleMapInfo {
-    /// The underlying kind of object pointed-to by handles
-    /// derived from the associated handle-map.
-    type Object: Send + Sync;
-
-    /// Gets the dimensions of the corresponding handle-map.
-    fn dimensions(&self) -> HandleMapDimensions;
-
-    /// Gets a static reference to the global handle-map
-    fn get_handle_map() -> &'static HandleMap<Self::Object>;
-}
-
-/// An individual handle to be given out by a [`HandleMap`].
-/// This representation is untyped, and just a wrapper
-/// around a handle-id, in contrast to implementors of `HandleLike`.
-#[derive(Clone, Copy, PartialEq, Eq, Hash)]
-pub struct Handle {
-    handle_id: u64,
-}
-
-impl From<&Handle> for Handle {
-    fn from(handle: &Handle) -> Self {
-        *handle
-    }
-}
-
-impl Handle {
-    /// Constructs a handle wrapping the given ID.
-    ///
-    /// No validity checks are done on the wrapped ID
-    /// to ensure that the given ID is active in
-    /// any specific handle-map, and the type
-    /// of the handle is not represented.
-    ///
-    /// As a result, this method is only useful for
-    /// allowing access to handles from an FFI layer.
-    pub fn from_id(handle_id: u64) -> Self {
-        Self { handle_id }
-    }
-    /// Gets the ID for this handle.
-    ///
-    /// Since the underlying handle is un-typed,`
-    /// this method is only suitable for
-    /// transmitting handles across an FFI layer.
-    pub fn get_id(&self) -> u64 {
-        self.handle_id
-    }
-    /// Derives the shard index from the handle id
-    fn get_shard_index(&self, num_shards: u8) -> usize {
-        (self.handle_id % (num_shards as u64)) as usize
-    }
-}
-
-/// Error raised when attempting to allocate into a full handle-map.
-#[derive(Debug)]
-pub struct HandleMapFullError;
-
-/// Internal error enum for failed allocations into a given shard.
-enum ShardAllocationError<T, F: FnOnce() -> T> {
-    /// Error for when the entry for the handle is occupied,
-    /// in which case we spit out the object-provider to try again
-    /// with a new handle-id.
-    EntryOccupied(F),
-    /// Error for when we would exceed the maximum number of allocations.
-    ExceedsAllocationLimit,
-}
-
-/// Error raised when the entry for a given [`Handle`] doesn't exist.
-#[derive(Debug)]
-pub struct HandleNotPresentError;
-
-/// Wrapper around a `HandleMap` which is specifically tailored for maintaining
-/// a global singleton `'static` reference to a handle-map.
-/// The wrapper handles initialization of the singleton in a const-context
-/// and subsequent accesses to ensure that the underlying map is always
-/// initialized upon attempts to access it.
-pub struct SingletonHandleMap<I: SingletonHandleMapInfo> {
-    info: I,
-    wrapped: spin::once::Once<HandleMap<I::Object>, spin::relax::Spin>,
-}
-
-impl<I: SingletonHandleMapInfo> SingletonHandleMap<I> {
-    /// Constructs a new global handle-map using the given
-    /// singleton handle-map info. This method is callable
-    /// from a `const`-context to allow it to be used to initialize
-    /// `static` variables.
-    pub const fn with_info(info: I) -> Self {
-        Self { info, wrapped: spin::once::Once::new() }
-    }
-    /// Initialize the handle-map if it's not already initialized,
-    /// and return a reference to the result.
-    pub fn get(&self) -> &HandleMap<I::Object> {
-        self.wrapped.call_once(|| {
-            let dimensions = self.info.dimensions();
-            HandleMap::with_dimensions(dimensions)
-        })
-    }
-}
-
-/// A thread-safe mapping from "handle"s [like pointers, but safer]
-/// to underlying structures, supporting allocations, reads, writes,
-/// and deallocations of objects behind handles.
-pub struct HandleMap<T: Send + Sync> {
-    /// The dimensions of this handle-map
-    dimensions: HandleMapDimensions,
-
-    /// The individually-lockable "shards" of the handle-map,
-    /// among which the keys will be roughly uniformly-distributed.
-    handle_map_shards: Box<[HandleMapShard<T>]>,
-
-    /// An atomically-incrementing counter which tracks the
-    /// next handle ID which allocations will attempt to use.
-    new_handle_id_counter: AtomicU64,
-
-    /// An atomic integer roughly tracking the number of
-    /// currently-outstanding allocated entries in this
-    /// handle-map among all [`HandleMapShard`]s.
-    outstanding_allocations_counter: AtomicU32,
-}
-
-impl<T: Send + Sync> HandleMap<T> {
-    /// Creates a new handle-map with the given `HandleMapDimensions`.
-    pub fn with_dimensions(dimensions: HandleMapDimensions) -> Self {
-        let mut handle_map_shards = Vec::with_capacity(dimensions.num_shards as usize);
-        for _ in 0..dimensions.num_shards {
-            handle_map_shards.push(HandleMapShard::default());
-        }
-        let handle_map_shards = handle_map_shards.into_boxed_slice();
-        Self {
-            dimensions,
-            handle_map_shards,
-            new_handle_id_counter: AtomicU64::new(0),
-            outstanding_allocations_counter: AtomicU32::new(0),
-        }
-    }
-}
-
-impl<T: Send + Sync> HandleMap<T> {
-    /// Allocates a new object within the given handle-map, returning
-    /// a handle to the location it was stored at. This operation
-    /// may fail if attempting to allocate over the `dimensions.max_active_handles`
-    /// limit imposed on the handle-map, in which case this method
-    /// will return a `HandleMapFullError`.
-    pub fn allocate(
-        &self,
-        initial_value_provider: impl FnOnce() -> T,
-    ) -> Result<Handle, HandleMapFullError> {
-        let mut initial_value_provider = initial_value_provider;
-        loop {
-            // Increment the new-handle-ID counter using relaxed memory ordering,
-            // since the only invariant that we want to enforce is that concurrently-running
-            // threads always get distinct new handle-ids.
-            let new_handle_id = self.new_handle_id_counter.fetch_add(1, Ordering::Relaxed);
-            let new_handle = Handle::from_id(new_handle_id);
-            let shard_index = new_handle.get_shard_index(self.dimensions.num_shards);
-
-            // Now, check the shard to see if we can actually allocate into it.
-            let shard_allocate_result = self.handle_map_shards[shard_index].try_allocate(
-                new_handle,
-                initial_value_provider,
-                &self.outstanding_allocations_counter,
-                self.dimensions.max_active_handles,
-            );
-            match shard_allocate_result {
-                Ok(_) => {
-                    return Ok(new_handle);
-                }
-                Err(ShardAllocationError::ExceedsAllocationLimit) => {
-                    return Err(HandleMapFullError);
-                }
-                Err(ShardAllocationError::EntryOccupied(thrown_back_provider)) => {
-                    // We need to do the whole thing again with a new ID
-                    initial_value_provider = thrown_back_provider;
-                }
-            }
-        }
-    }
-
-    /// Gets a read-only reference to an object within the given handle-map,
-    /// if the given handle is present. Otherwise, returns [`HandleNotPresentError`].
-    pub fn get(&self, handle: Handle) -> Result<ObjectReadGuard<T>, HandleNotPresentError> {
-        let shard_index = handle.get_shard_index(self.dimensions.num_shards);
-        self.handle_map_shards[shard_index].get(handle)
-    }
-
-    /// Gets a read+write reference to an object within the given handle-map,
-    /// if the given handle is present. Otherwise, returns [`HandleNotPresentError`].
-    pub fn get_mut(
-        &self,
-        handle: Handle,
-    ) -> Result<ObjectReadWriteGuard<T>, HandleNotPresentError> {
-        let shard_index = handle.get_shard_index(self.dimensions.num_shards);
-        self.handle_map_shards[shard_index].get_mut(handle)
-    }
-
-    /// Removes the object pointed to by the given handle in
-    /// the handle-map, returning the removed object if it
-    /// exists. Otherwise, returns [`HandleNotPresentError`].
-    pub fn deallocate(&self, handle: Handle) -> Result<T, HandleNotPresentError> {
-        let shard_index = handle.get_shard_index(self.dimensions.num_shards);
-        self.handle_map_shards[shard_index]
-            .deallocate(handle, &self.outstanding_allocations_counter)
-    }
-
-    /// Gets the actual number of elements stored in the entire map.
-    /// Only suitable for single-threaded sections of tests.
-    #[cfg(test)]
-    pub(crate) fn len(&self) -> usize {
-        self.handle_map_shards.iter().map(|s| s.len()).sum()
-    }
-
-    /// Sets the new-handle-id counter to the given value.
-    /// Only suitable for tests.
-    #[cfg(test)]
-    pub(crate) fn set_new_handle_id_counter(&mut self, value: u64) {
-        self.new_handle_id_counter = AtomicU64::new(value);
-    }
-}
-
-// Bunch o' type aliases to make talking about them much easier in the shard code.
-type ShardMapType<T> = hashbrown::HashMap<Handle, T>;
-type ShardReadWriteLock<T> = RwLock<ShardMapType<T>>;
-type ShardReadGuard<'a, T> = RwLockReadGuard<'a, ShardMapType<T>>;
-type ShardUpgradableReadGuard<'a, T> = RwLockUpgradableReadGuard<'a, ShardMapType<T>>;
-type ShardReadWriteGuard<'a, T> = RwLockWriteGuard<'a, ShardMapType<T>>;
-
-/// An individual handle-map shard, which is ultimately
-/// just a hash-map behind a lock.
-pub(crate) struct HandleMapShard<T: Send + Sync> {
-    data: RwLock<ShardMapType<T>>,
-}
-
-impl<T: Send + Sync> Default for HandleMapShard<T> {
-    fn default() -> Self {
-        Self { data: RwLock::new(hashbrown::HashMap::new()) }
-    }
-}
-
-impl<T: Send + Sync> HandleMapShard<T> {
-    fn get(&self, handle: Handle) -> Result<ObjectReadGuard<T>, HandleNotPresentError> {
-        let map_read_guard = ShardReadWriteLock::<T>::read(&self.data);
-        let read_only_map_ref = map_read_guard.deref();
-        if read_only_map_ref.contains_key(&handle) {
-            let object_read_guard = ShardReadGuard::<T>::map(map_read_guard, move |map_ref| {
-                // We know that the entry exists, since we've locked the
-                // shard and already checked that it exists prior to
-                // handing out this new, mapped read-lock.
-                map_ref.get(&handle).unwrap()
-            });
-            Ok(ObjectReadGuard { guard: object_read_guard })
-        } else {
-            // Auto-drop the read guard, and return an error
-            Err(HandleNotPresentError)
-        }
-    }
-    /// Gets a read-write guard on the entire shard map if an entry for the given
-    /// handle exists, but if not, yield [`HandleNotPresentError`].
-    fn get_read_write_guard_if_entry_exists(
-        &self,
-        handle: Handle,
-    ) -> Result<ShardReadWriteGuard<T>, HandleNotPresentError> {
-        // Start with an upgradable read lock and then upgrade to a write lock.
-        // By doing this, we prevent new readers from entering (see `spin` documentation)
-        let map_upgradable_read_guard = ShardReadWriteLock::<T>::upgradable_read(&self.data);
-        let read_only_map_ref = map_upgradable_read_guard.deref();
-        if read_only_map_ref.contains_key(&handle) {
-            // If we know that the entry exists, and we're currently
-            // holding a read-lock, we know that we're safe to request
-            // an upgrade to a write lock, since only one write or
-            // upgradable read lock can be outstanding at any one time.
-            let map_read_write_guard =
-                ShardUpgradableReadGuard::<T>::upgrade(map_upgradable_read_guard);
-            Ok(map_read_write_guard)
-        } else {
-            // Auto-drop the read guard, we don't need to allow a write.
-            Err(HandleNotPresentError)
-        }
-    }
-
-    fn get_mut(&self, handle: Handle) -> Result<ObjectReadWriteGuard<T>, HandleNotPresentError> {
-        let map_read_write_guard = self.get_read_write_guard_if_entry_exists(handle)?;
-        // Expose only the pointed-to object with a mapped read-write guard
-        let object_read_write_guard =
-            ShardReadWriteGuard::<T>::map(map_read_write_guard, move |map_ref| {
-                // Already checked that the entry exists while holding the lock
-                map_ref.get_mut(&handle).unwrap()
-            });
-        Ok(ObjectReadWriteGuard { guard: object_read_write_guard })
-    }
-    fn deallocate(
-        &self,
-        handle: Handle,
-        outstanding_allocations_counter: &AtomicU32,
-    ) -> Result<T, HandleNotPresentError> {
-        let mut map_read_write_guard = self.get_read_write_guard_if_entry_exists(handle)?;
-        // We don't need to worry about double-decrements, since the above call
-        // got us an upgradable read guard for our read, which means it's the only
-        // outstanding upgradeable guard on the shard. See `spin` documentation.
-        // Remove the pointed-to object from the map, and return it,
-        // releasing the lock when the guard goes out of scope.
-        let removed_object = map_read_write_guard.deref_mut().remove(&handle).unwrap();
-        // Decrement the allocations counter. Release ordering because we want
-        // to ensure that clearing the map entry never gets re-ordered to after when
-        // this counter gets decremented.
-        outstanding_allocations_counter.sub(1, Ordering::Release);
-        Ok(removed_object)
-    }
-
-    fn try_allocate<F>(
-        &self,
-        handle: Handle,
-        object_provider: F,
-        outstanding_allocations_counter: &AtomicU32,
-        max_active_handles: u32,
-    ) -> Result<(), ShardAllocationError<T, F>>
-    where
-        F: FnOnce() -> T,
-    {
-        // Use an upgradeable read guard -> write guard to provide greater fairness
-        // toward writers (see `spin` documentation)
-        let map_upgradable_read_guard = ShardReadWriteLock::<T>::upgradable_read(&self.data);
-        let mut map_read_write_guard =
-            ShardUpgradableReadGuard::<T>::upgrade(map_upgradable_read_guard);
-        match map_read_write_guard.entry_ref(&handle) {
-            EntryRef::Occupied(_) => {
-                // We've already allocated for that handle-id, so yield
-                // the object provider back to the caller.
-                Err(ShardAllocationError::EntryOccupied(object_provider))
-            }
-            EntryRef::Vacant(vacant_entry) => {
-                // An entry is open, but we haven't yet checked the allocations count.
-                // Try to increment the total allocations count atomically.
-                // Use acquire ordering on a successful bump, because we don't want
-                // to invoke the allocation closure before we have a guaranteed slot.
-                // On the other hand, upon failure, we don't care about ordering
-                // of surrounding operations, and so we use a relaxed ordering there.
-                let allocation_count_bump_result = outstanding_allocations_counter.fetch_update(
-                    Ordering::Acquire,
-                    Ordering::Relaxed,
-                    |old_total_allocations| {
-                        if old_total_allocations >= max_active_handles {
-                            None
-                        } else {
-                            Some(old_total_allocations + 1)
-                        }
-                    },
-                );
-                match allocation_count_bump_result {
-                    Ok(_) => {
-                        // We're good to actually allocate
-                        let object = object_provider();
-                        vacant_entry.insert(object);
-                        Ok(())
-                    }
-                    Err(_) => {
-                        // The allocation would cause us to exceed the allowed allocations,
-                        // so release all locks and error.
-                        Err(ShardAllocationError::ExceedsAllocationLimit)
-                    }
-                }
-            }
-        }
-    }
-    /// Gets the actual number of elements stored in this shard.
-    /// Only suitable for single-threaded sections of tests.
-    #[cfg(test)]
-    fn len(&self) -> usize {
-        let guard = ShardReadWriteLock::<T>::read(&self.data);
-        guard.deref().len()
-    }
-}
-
-/// Externally-facing trait for things which behave like handle-map handles
-/// with a globally-defined handle-map for the type.
-pub trait HandleLike: Sized {
-    /// The underlying object type pointed-to by this handle
-    type Object: Send + Sync;
-
-    /// Tries to allocate a new handle using the given provider
-    /// to construct the underlying stored object as a new
-    /// entry into the global handle table for this type.
-    fn allocate(
-        initial_value_provider: impl FnOnce() -> Self::Object,
-    ) -> Result<Self, HandleMapFullError>;
-
-    /// Gets a RAII read-guard on the contents behind this handle.
-    fn get(&self) -> Result<ObjectReadGuard<Self::Object>, HandleNotPresentError>;
-
-    /// Gets a RAII read-write guard on the contents behind this handle.
-    fn get_mut(&self) -> Result<ObjectReadWriteGuard<Self::Object>, HandleNotPresentError>;
-
-    /// Deallocates the contents behind this handle.
-    fn deallocate(self) -> Result<Self::Object, HandleNotPresentError>;
-}
-
-#[macro_export]
-/// `declare_handle_map! { handle_module_name, handle_type_name, wrapped_type,
-/// map_dimension_provider }`
-///
-/// Declares a new public module with name `handle_module_name` which includes a new type
-/// `handle_type_name` which is `#[repr(C)]` and represents FFI-accessible handles
-/// to values of type `wrapped_type`.
-///
-/// Internal to the generated module, a new static `SingletonHandleMap` is created, where the
-/// maximum number of active handles and the number of shards are given by
-/// the dimensions returned by evaluation of the `map_dimension_provider` expression.
-///
-/// Note: `map_dimension_provider` will be evaluated within the defined module's scope,
-/// so you will likely need to use `super` to refer to definitions in the enclosing scope.
-///
-/// # Example
-/// The following code defines an FFI-safe type `StringHandle` which references
-/// the `String` data-type, and uses it to define a (contrived)
-/// function `sample` which will print "Hello World".
-///
-/// ```
-/// mod sample {
-///     use core::ops::Deref;
-///     use handle_map::{declare_handle_map, HandleMapDimensions, HandleLike};
-///
-///     fn get_string_handle_map_dimensions() -> HandleMapDimensions {
-///         HandleMapDimensions {
-///             num_shards: 8,
-///             max_active_handles: 100,
-///         }
-///     }
-///
-///     declare_handle_map! { string_handle, StringHandle, String,
-///                           super::get_string_handle_map_dimensions() }
-///
-///     use string_handle::StringHandle;
-///
-///     fn sample() {
-///         // Note: this method could panic if there are
-///         // more than 99 outstanding handles.
-///
-///         // Allocate a new string-handle pointing to the string "Hello"
-///         let handle = StringHandle::allocate(|| { "Hello".to_string() }).unwrap();
-///         {
-///             // Obtain a write-guard on the contents of our handle
-///             let mut handle_write_guard = handle.get_mut().unwrap();
-///             handle_write_guard.push_str(" World");
-///             // Write guard is auto-dropped at the end of this block.
-///         }
-///         {
-///             // Obtain a read-guard on the contents of our handle.
-///             // Note that we had to ensure that the write-guard was
-///             // dropped prior to doing this, or else execution
-///             // could potentially hang.
-///             let handle_read_guard = handle.get().unwrap();
-///             println!("{}", handle_read_guard.deref());
-///         }
-///         // Clean up the data behind the created handle
-///         handle.deallocate().unwrap();
-///     }
-/// }
-/// ```
-macro_rules! declare_handle_map {
-    ($handle_module_name:ident, $handle_type_name:ident, $wrapped_type:ty,
-     $map_dimension_provider:expr) => {
-        #[doc = concat!("Macro-generated (via `handle_map::declare_handle_map!`) module",
-                        " which defines the `", stringify!($handle_module_name), "::",
-                        stringify!($handle_type_name), "` FFI-transmissible handle type ",
-                        " which references values of type `", stringify!($wrapped_type), "`.")]
-        pub mod $handle_module_name {
-            use $crate::SingletonHandleMapInfo;
-
-            struct SingletonInfo;
-
-            static GLOBAL_HANDLE_MAP: $crate::SingletonHandleMap<SingletonInfo> =
-                $crate::SingletonHandleMap::with_info(SingletonInfo);
-
-            impl $crate::SingletonHandleMapInfo for SingletonInfo {
-                type Object = $wrapped_type;
-
-                fn dimensions(&self) -> $crate::HandleMapDimensions {
-                    $map_dimension_provider
-                }
-                fn get_handle_map() -> &'static $crate::HandleMap<$wrapped_type> {
-                    GLOBAL_HANDLE_MAP.get()
-                }
-            }
-
-            #[doc = concat!("A `#[repr(C)]` handle to a value of type `",
-                            stringify!($wrapped_type), "`.")]
-            #[repr(C)]
-            #[derive(Clone, Copy, PartialEq, Eq)]
-            pub struct $handle_type_name {
-                handle_id: u64,
-            }
-
-            impl $handle_type_name {
-                fn get_as_handle(&self) -> $crate::Handle {
-                    $crate::Handle::from_id(self.handle_id)
-                }
-            }
-            impl $crate::HandleLike for $handle_type_name {
-                type Object = $wrapped_type;
-                fn allocate(initial_value_provider: impl FnOnce() -> $wrapped_type,
-                ) -> Result<Self, $crate::HandleMapFullError> {
-                    SingletonInfo::get_handle_map().allocate(initial_value_provider)
-                        .map(|derived_handle| Self {
-                            handle_id: derived_handle.get_id(),
-                        })
-                }
-                fn get(&self) -> Result<$crate::ObjectReadGuard<$wrapped_type>, $crate::HandleNotPresentError> {
-                    SingletonInfo::get_handle_map().get(self.get_as_handle())
-                }
-                fn get_mut(&self) -> Result<$crate::ObjectReadWriteGuard<$wrapped_type>, $crate::HandleNotPresentError> {
-                    SingletonInfo::get_handle_map().get_mut(self.get_as_handle())
-                }
-                fn deallocate(self) -> Result<$wrapped_type, $crate::HandleNotPresentError> {
-                    SingletonInfo::get_handle_map().deallocate(self.get_as_handle())
-                }
-            }
-        }
-    }
-}
diff --git a/nearby/presence/ldt/Cargo.toml b/nearby/presence/ldt/Cargo.toml
index c4456cc..2ee7b4c 100644
--- a/nearby/presence/ldt/Cargo.toml
+++ b/nearby/presence/ldt/Cargo.toml
@@ -13,7 +13,7 @@
 ldt_tbc.workspace = true
 
 [dev-dependencies]
-crypto_provider_default = {workspace = true}
+crypto_provider_default = {workspace = true, features = ["rustcrypto"]}
 rand_ext.workspace = true
 test_helper.workspace = true
 xts_aes.workspace = true
diff --git a/nearby/presence/ldt/fuzz/Cargo.lock b/nearby/presence/ldt/fuzz/Cargo.lock
index 767108f..419d9e9 100644
--- a/nearby/presence/ldt/fuzz/Cargo.lock
+++ b/nearby/presence/ldt/fuzz/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -157,6 +171,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_rustcrypto"
@@ -164,6 +181,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -193,9 +211,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -261,9 +279,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -329,6 +347,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -583,6 +611,12 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
 name = "typenum"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -618,9 +652,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/presence/ldt/src/lib.rs b/nearby/presence/ldt/src/lib.rs
index f05c14b..9065e12 100644
--- a/nearby/presence/ldt/src/lib.rs
+++ b/nearby/presence/ldt/src/lib.rs
@@ -31,7 +31,6 @@
 /// `B` is the block size.
 /// `T` is the provided implementation of a Tweakable Block Cipher
 /// `M` is the implementation of a [pure mix function](https://eprint.iacr.org/2017/841.pdf)
-#[repr(C)]
 pub struct LdtEncryptCipher<const B: usize, T: TweakableBlockCipher<B>, M: Mix> {
     cipher_1: T::EncryptionCipher,
     cipher_2: T::EncryptionCipher,
@@ -124,7 +123,7 @@
     // Encrypt or decrypt in place with a tweak
     O: Fn(&C, T::Tweak, &mut [u8; B]),
     // Mix a/b into block-sized chunks
-    X: Fn(&[u8], &[u8]) -> ([u8; B], [u8; B]),
+    X: for<'a, 'b> Fn(&'a [u8], &'b [u8]) -> (&'b [u8], &'a [u8]),
     P: Padder<B, T>,
 {
     if data.len() < B || data.len() >= B * 2 {
@@ -147,20 +146,23 @@
     // |z| = B - s, |m3| = s
     let (z, m3) = m1_ciphertext.split_at(B - s);
     debug_assert_eq!(s, m3.len());
+
     // c3 and c2 are the last s bytes of their size-B arrays, respectively
-    let (mut c3, c2) = mix(m3, m2);
+    let (c3, c2) = mix(m3, m2);
+
     let c1 = {
-        // constructing z || c3 is easy since c3 is already the last s bytes
-        c3[0..(B - s)].copy_from_slice(z);
-        let mut z_c3 = c3;
-        let tweak = padder.pad_tweak(&c2[B - s..]);
+        let mut z_c3 = [0; B];
+        z_c3[(B - s)..].copy_from_slice(c3);
+        z_c3[0..(B - s)].copy_from_slice(z);
+
+        let tweak = padder.pad_tweak(c2);
         cipher_op(second_cipher, tweak, &mut z_c3);
         z_c3
     };
-    let len = data.len();
-    data.get_mut(0..B).ok_or(LdtError::InvalidLength(len))?.copy_from_slice(&c1);
-    data.get_mut(B..).ok_or(LdtError::InvalidLength(len))?.copy_from_slice(&c2[B - s..]);
 
+    let (left, right) = data.split_at_mut(B);
+    left.copy_from_slice(&c1);
+    right.copy_from_slice(c2);
     Ok(())
 }
 
@@ -219,31 +221,23 @@
     /// Mix `a` and `b`, writing into the last `s` bytes of the output arrays.
     /// `a` and `b` must be the same length `s`, and no longer than the block size `B`.
     /// Must be the inverse of [Mix::backwards].
-    fn forwards<const B: usize>(a: &[u8], b: &[u8]) -> ([u8; B], [u8; B]);
+    fn forwards<'a, 'b>(a: &'a [u8], b: &'b [u8]) -> (&'b [u8], &'a [u8]);
 
     /// Mix `a` and `b`, writing into the last `s` bytes of the output arrays.
     /// `a` and `b` must be the same length, and no longer than the block size `B`.
     /// Must be the inverse of [Mix::forwards].
-    fn backwards<const B: usize>(a: &[u8], b: &[u8]) -> ([u8; B], [u8; B]);
+    fn backwards<'a, 'b>(a: &'a [u8], b: &'b [u8]) -> (&'b [u8], &'a [u8]);
 }
 
 /// Per section 2.4, swapping `a` and `b` is a valid mix function
 pub struct Swap {}
 impl Mix for Swap {
-    fn forwards<const B: usize>(a: &[u8], b: &[u8]) -> ([u8; B], [u8; B]) {
+    fn forwards<'a, 'b>(a: &'a [u8], b: &'b [u8]) -> (&'b [u8], &'a [u8]) {
         debug_assert_eq!(a.len(), b.len());
-        // implies b length as well
-        debug_assert!(a.len() <= B);
-        let mut out1 = [0; B];
-        let mut out2 = [0; B];
-
-        let start = B - a.len();
-        out1[start..].copy_from_slice(b);
-        out2[start..].copy_from_slice(a);
-        (out1, out2)
+        (b, a)
     }
 
-    fn backwards<const B: usize>(a: &[u8], b: &[u8]) -> ([u8; B], [u8; B]) {
+    fn backwards<'a, 'b>(a: &'a [u8], b: &'b [u8]) -> (&'b [u8], &'a [u8]) {
         // backwards is the same as forwards.
         Self::forwards(a, b)
     }
@@ -262,7 +256,7 @@
     fn pad_tweak(&self, data: &[u8]) -> T::Tweak;
 }
 
-/// The default padding algorithm per section 2 of LDT paper.
+/// The default padding algorithm per section 2 of [LDT paper](https://eprint.iacr.org/2017/841.pdf)
 #[derive(Default)]
 pub struct DefaultPadder;
 
diff --git a/nearby/presence/ldt_np_adv/Cargo.toml b/nearby/presence/ldt_np_adv/Cargo.toml
index 02592ce..522e97b 100644
--- a/nearby/presence/ldt_np_adv/Cargo.toml
+++ b/nearby/presence/ldt_np_adv/Cargo.toml
@@ -17,14 +17,14 @@
 ldt_tbc.workspace = true
 
 [dev-dependencies]
-crypto_provider_default.workspace = true
+crypto_provider_default = { workspace = true, features = ["rustcrypto", "std"] }
 crypto_provider_openssl.workspace = true
 rand_ext.workspace = true
 test_helper.workspace = true
 
 rand.workspace = true
 base64.workspace = true
-serde_json = {workspace = true, features=["std"]}
+serde_json = { workspace = true, features = ["std"] }
 hex.workspace = true
 anyhow.workspace = true
 criterion.workspace = true
diff --git a/nearby/presence/ldt_np_adv/fuzz/Cargo.lock b/nearby/presence/ldt_np_adv/fuzz/Cargo.lock
index 5050ad0..6b6ef4b 100644
--- a/nearby/presence/ldt_np_adv/fuzz/Cargo.lock
+++ b/nearby/presence/ldt_np_adv/fuzz/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -161,6 +175,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_rustcrypto"
@@ -168,6 +185,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -197,9 +215,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -265,9 +283,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -333,6 +351,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -610,6 +638,12 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
 name = "typenum"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -645,9 +679,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/presence/ldt_np_adv/src/lib.rs b/nearby/presence/ldt_np_adv/src/lib.rs
index 3785797..31aa50d 100644
--- a/nearby/presence/ldt_np_adv/src/lib.rs
+++ b/nearby/presence/ldt_np_adv/src/lib.rs
@@ -41,10 +41,13 @@
 
 /// Max LDT-XTS-AES data size: `(2 * AES block size) - 1`
 pub const LDT_XTS_AES_MAX_LEN: usize = 31;
-/// Legacy (v0) format uses 14-byte metadata key
+
+/// Legacy (v0) format uses a 14-byte metadata key
 pub const NP_LEGACY_METADATA_KEY_LEN: usize = 14;
 
-/// The salt included in an NP advertisement
+/// The salt included in an NP advertisement.
+/// LDT does not use an IV but can instead incorporate the 2 byte, regularly rotated,
+/// salt from the advertisement payload and XOR it with the padded tweak data.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub struct LegacySalt {
     /// Salt bytes extracted from the incoming NP advertisement
@@ -104,6 +107,20 @@
 
 /// Decrypts and validates a NP legacy format advertisement encrypted with LDT.
 ///
+/// A NP legacy advertisement will always be in the format of:
+///
+/// Header (1 byte) | Identity DE header (1 byte) | Salt (2 bytes) | Identity (14 bytes) | repeated
+/// { DE header | DE payload }
+///
+/// Example:
+/// Header (1 byte) | Identity DE header (1 byte) | Salt (2 bytes) | Identity (14 bytes) |
+/// Tx power DE header (1 byte) | Tx power (1 byte) | Action DE header(1 byte) | action (1-3 bytes)
+///
+/// The ciphertext bytes will always start with the Identity through the end of the
+/// advertisement, for example in the above [ Identity (14 bytes) | Tx power DE header (1 byte) |
+/// Tx power (1 byte) | Action DE header(1 byte) | action (1-3 bytes) ] will be the ciphertext section
+/// passed as the input to `decrypt_and_verify`
+///
 /// `B` is the underlying block cipher block size.
 /// `O` is the max output size (must be 2 * B - 1).
 /// `T` is the tweakable block cipher used by LDT.
@@ -130,6 +147,10 @@
     ///
     /// If the plaintext's metadata key matches this item's MAC, return the plaintext, otherwise `None`.
     ///
+    /// NOTE: because LDT acts as a PRP over the entire message, tampering with any bit scrambles
+    /// the whole message, so we can leverage the MAC on just the metadata key to ensure integrity
+    /// for the whole message.
+    ///
     /// # Errors
     /// - If `payload` has a length outside of `[B, B * 2)`.
     /// - If the decrypted plaintext fails its HMAC validation
diff --git a/nearby/presence/ldt_np_adv_ffi/.cargo/config-boringssl.toml b/nearby/presence/ldt_np_adv_ffi/.cargo/config-boringssl.toml
deleted file mode 100644
index 5a4a047..0000000
--- a/nearby/presence/ldt_np_adv_ffi/.cargo/config-boringssl.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-paths = [
-    "../../../boringssl-build/boringssl/rust/bssl-crypto",
-]
diff --git a/nearby/presence/ldt_np_adv_ffi/Cargo.lock b/nearby/presence/ldt_np_adv_ffi/Cargo.lock
index 1d13887..3f7e2a7 100644
--- a/nearby/presence/ldt_np_adv_ffi/Cargo.lock
+++ b/nearby/presence/ldt_np_adv_ffi/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -98,6 +112,13 @@
 [[package]]
 name = "bssl-crypto"
 version = "0.1.0"
+dependencies = [
+ "bssl-sys",
+]
+
+[[package]]
+name = "bssl-sys"
+version = "0.1.0"
 
 [[package]]
 name = "bytes"
@@ -177,6 +198,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_boringssl"
@@ -184,7 +208,6 @@
 dependencies = [
  "bssl-crypto",
  "crypto_provider",
- "crypto_provider_stubs",
 ]
 
 [[package]]
@@ -204,6 +227,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -240,9 +264,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -299,9 +323,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -384,6 +408,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -841,6 +875,12 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
 name = "typenum"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -882,9 +922,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/presence/ldt_np_adv_ffi/Cargo.toml b/nearby/presence/ldt_np_adv_ffi/Cargo.toml
index d53809f..829f932 100644
--- a/nearby/presence/ldt_np_adv_ffi/Cargo.toml
+++ b/nearby/presence/ldt_np_adv_ffi/Cargo.toml
@@ -6,20 +6,20 @@
 
 [dependencies]
 crypto_provider = { path = "../../crypto/crypto_provider" }
-crypto_provider_openssl = {path = "../../crypto/crypto_provider_openssl", optional = true}
-crypto_provider_rustcrypto = {path = "../../crypto/crypto_provider_rustcrypto", optional = true}
-crypto_provider_boringssl = {path = "../../crypto/crypto_provider_boringssl", optional = true}
+crypto_provider_openssl = { path = "../../crypto/crypto_provider_openssl", optional = true }
+crypto_provider_rustcrypto = { path = "../../crypto/crypto_provider_rustcrypto", optional = true }
+crypto_provider_boringssl = { path = "../../crypto/crypto_provider_boringssl", optional = true }
 ldt = { path = "../ldt" }
 ldt_np_adv = { path = "../ldt_np_adv" }
-np_hkdf = {path = "../np_hkdf"}
+np_hkdf = { path = "../np_hkdf" }
 
 cfg-if = "1.0.0"
-lazy_static = { version = "1.4.0"}
+lazy_static = { version = "1.4.0" }
 
 # no_std only deps
-libc_alloc = {version = "1.0.4", optional = true }
+libc_alloc = { version = "1.0.4", optional = true }
 panic-abort = { version = "0.3.2", optional = true }
-spin = {version = "0.9.8", optional = true }
+spin = { version = "0.9.8", optional = true }
 
 [dev-dependencies]
 rand = "0.8.5"
diff --git a/nearby/presence/ldt_np_adv_ffi/src/lib.rs b/nearby/presence/ldt_np_adv_ffi/src/lib.rs
index 2ed8d95..1534857 100644
--- a/nearby/presence/ldt_np_adv_ffi/src/lib.rs
+++ b/nearby/presence/ldt_np_adv_ffi/src/lib.rs
@@ -20,6 +20,8 @@
     clippy::panic,
     clippy::expect_used
 )]
+#![allow(internal_features)]
+// TODO: Remove usage of `lang_items` when ffi is no longer alloc
 // These features are needed to support no_std + alloc
 #![feature(lang_items)]
 
@@ -128,7 +130,7 @@
         get_enc_handle_map()
             .remove(&handle.handle)
             .ok_or(CloseCipherError::InvalidHandle)
-            .map(|_| 0)
+            .map(|_| SUCCESS)
     })
 }
 
@@ -138,14 +140,11 @@
         get_dec_handle_map()
             .remove(&handle.handle)
             .ok_or(CloseCipherError::InvalidHandle)
-            .map(|_| 0)
+            .map(|_| SUCCESS)
     })
 }
 
 #[no_mangle]
-// continue to use LdtAdvDecrypter::encrypt() for now, but we should expose a higher level API
-// and get rid of this.
-#[allow(deprecated)]
 extern "C" fn NpLdtEncrypt(
     handle: NpLdtEncryptHandle,
     buffer: *mut u8,
diff --git a/nearby/presence/ldt_np_c_sample/CMakeLists.txt b/nearby/presence/ldt_np_c_sample/CMakeLists.txt
index 588078a..aefc0ba 100644
--- a/nearby/presence/ldt_np_c_sample/CMakeLists.txt
+++ b/nearby/presence/ldt_np_c_sample/CMakeLists.txt
@@ -31,7 +31,6 @@
     ldt_c_sample
     optimized "${CMAKE_SOURCE_DIR}/ldt_np_adv_ffi/target/release/${CMAKE_STATIC_LIBRARY_PREFIX}ldt_np_adv_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}"
     debug "${CMAKE_SOURCE_DIR}/ldt_np_adv_ffi/target/debug/${CMAKE_STATIC_LIBRARY_PREFIX}ldt_np_adv_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}"
-    OpenSSL::SSL
 )
 
 if(UNIX)
diff --git a/nearby/presence/ldt_np_c_sample/main.c b/nearby/presence/ldt_np_c_sample/main.c
index 1ae9244..9348a70 100644
--- a/nearby/presence/ldt_np_c_sample/main.c
+++ b/nearby/presence/ldt_np_c_sample/main.c
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
+#include "np_ldt.h"
+
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
-#include "np_ldt.h"
-
 static const uint8_t KEY_SEED_BYTES[] = {204, 219, 36, 137, 233, 252, 172, 66, 179, 147, 72, 184, 148, 30, 209, 154, 29, 54, 14, 117, 224, 152, 200, 193, 94, 107, 28, 194, 182, 32, 205, 57};
 static const uint8_t KNOWN_HMAC_BYTES[] = {223, 185, 10, 31, 155, 31, 226, 141, 24, 187, 204, 165, 34, 64, 181, 204, 44, 203, 95, 141, 82, 137, 163, 203, 100, 235, 53, 65, 202, 97, 75, 180};
 static const uint8_t TEST_DATA_BYTES[] = {205, 104, 63, 225, 161, 209, 248, 70, 84, 61, 10, 19, 212, 174, 164, 0, 64, 200, 214, 123};
diff --git a/nearby/presence/ldt_np_c_sample/tests/CMakeLists.txt b/nearby/presence/ldt_np_c_sample/tests/CMakeLists.txt
index 6edfd63..141563d 100644
--- a/nearby/presence/ldt_np_c_sample/tests/CMakeLists.txt
+++ b/nearby/presence/ldt_np_c_sample/tests/CMakeLists.txt
@@ -25,7 +25,6 @@
   "${CMAKE_SOURCE_DIR}/ldt_np_adv_ffi/target/release/${CMAKE_STATIC_LIBRARY_PREFIX}ldt_np_adv_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}"
   jsoncpp
   GTest::gtest_main
-  OpenSSL::SSL
 )
 if(UNIX)
   target_link_libraries(
@@ -51,7 +50,6 @@
   ldt_benchmarks
   "${CMAKE_SOURCE_DIR}/ldt_np_adv_ffi/target/release/${CMAKE_STATIC_LIBRARY_PREFIX}ldt_np_adv_ffi${CMAKE_STATIC_LIBRARY_SUFFIX}"
   benchmark::benchmark
-  OpenSSL::SSL
 )
 
 if(UNIX)
diff --git a/nearby/presence/ldt_np_c_sample/tests/ldt_benchmarks.cc b/nearby/presence/ldt_np_c_sample/tests/ldt_benchmarks.cc
index d0cfb67..2559954 100644
--- a/nearby/presence/ldt_np_c_sample/tests/ldt_benchmarks.cc
+++ b/nearby/presence/ldt_np_c_sample/tests/ldt_benchmarks.cc
@@ -12,10 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <benchmark/benchmark.h>
+#include "np_ldt.h"
+
 #include <ctime>
 
-#include "np_ldt.h"
+#include "benchmark/benchmark.h"
 
 using std::vector;
 using std::tuple;
diff --git a/nearby/presence/ldt_np_c_sample/tests/ldt_ffi_tests.cc b/nearby/presence/ldt_np_c_sample/tests/ldt_ffi_tests.cc
index e66af65..7cd1fb3 100644
--- a/nearby/presence/ldt_np_c_sample/tests/ldt_ffi_tests.cc
+++ b/nearby/presence/ldt_np_c_sample/tests/ldt_ffi_tests.cc
@@ -12,9 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <gtest/gtest.h>
-#include <json/json.h>
-
 extern "C" {
 #include "np_ldt.h"
 }
@@ -27,6 +24,9 @@
 #include <pthread.h>
 #endif
 
+#include "gtest/gtest.h"
+#include "json/json.h"
+
 #ifdef LDT_TEST_VECTORS
 static const char *PATH_TO_DATA_FILE = LDT_TEST_VECTORS;
 #else
diff --git a/nearby/presence/ldt_np_jni/java/LdtNpJni/AndroidManifest.xml b/nearby/presence/ldt_np_jni/java/LdtNpJni/AndroidManifest.xml
new file mode 100644
index 0000000..978460f
--- /dev/null
+++ b/nearby/presence/ldt_np_jni/java/LdtNpJni/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2022 Google LLC
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.nearby.LdtNpJni">
+    <uses-sdk android:minSdkVersion="19" />
+</manifest>
\ No newline at end of file
diff --git a/nearby/presence/ldt_np_jni/java/LdtNpJni/build.gradle.kts b/nearby/presence/ldt_np_jni/java/LdtNpJni/build.gradle.kts
index 3cc0692..0f0991e 100644
--- a/nearby/presence/ldt_np_jni/java/LdtNpJni/build.gradle.kts
+++ b/nearby/presence/ldt_np_jni/java/LdtNpJni/build.gradle.kts
@@ -15,24 +15,35 @@
  */
 
 plugins {
-    id("java")
-    kotlin("jvm") version "1.8.0"
+  id("java")
+  kotlin("jvm") version "1.9.0"
+}
+
+kotlin {
+  jvmToolchain {
+    languageVersion.set(JavaLanguageVersion.of("17"))
+  }
 }
 
 group = "com.google.android.gms.nearby.presence.hazmat"
 version = "1.0-SNAPSHOT"
 
 repositories {
-    mavenCentral()
+  mavenCentral()
+  google()
 }
 
 dependencies {
-    testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
-    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
-    implementation(kotlin("stdlib"))
+  testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
+  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1")
+  implementation(kotlin("stdlib"))
+  implementation("androidx.annotation:annotation:1.6.0")
 }
 
 tasks.getByName<Test>("test") {
-    useJUnitPlatform()
-    jvmArgs = mutableListOf("-Djava.library.path=../../../../target/debug")
+  useJUnitPlatform()
+  jvmArgs = mutableListOf("-Djava.library.path=../../../../target/debug")
+  testLogging {
+    events("passed", "skipped", "failed")
+  }
 }
\ No newline at end of file
diff --git a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpCipher.java b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpCipher.java
deleted file mode 100644
index ecd0a85..0000000
--- a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpCipher.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Copyright 2022 Google LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.gms.nearby.presence.hazmat;
-
-/**
- * LDT-XTS-AES128 implementation using the default "swap" mix function. It is suitable for
- * encryption or decryption of individual Nearby Presence BLE 4.2 legacy format encrypted payloads.
- *
- * <p>To avoid leaking resources, call close() once an instance is no longer needed.
- *
- * <p>This class is not thread safe.
- */
-public class LdtNpCipher implements AutoCloseable {
-
-  /**
-   * Size in bytes of key seed used to derive further keys used for in np ldt operations
-   */
-  private static final int KEY_SEED_SIZE = 32;
-   /**
-   * Size in bytes of the metadata keys calculated hmac tag
-   */
-  private static final int TAG_SIZE = 32;
-  /** Block size of AES. */
-  private static final int BLOCK_SIZE = 16;
-
-  private final long ldtHandle;
-  private boolean closed = false;
-
-  private LdtNpCipher(long ldtHandle) {
-    this.ldtHandle = ldtHandle;
-  }
-
-  /**
-   * Create a new Ldt instance using LDT-XTS-AES128 with the "swap" mix function.
-   *
-   * @param key_seed 64-byte key material from the credential for the identity used to broadcast. The
-   *     supplied byte[] can be zeroed out once this method returns, as the contents are copied.
-   * @return an instance configured with the supplied key
-   * @throws LdtException if the key is the wrong size
-   * @throws LdtException if the tag is the wrong size
-   * @throws LdtException if creating the instance fails
-   */
-  public static LdtNpCipher fromKey(byte[] key_seed, byte[] metadata_key_tag) throws LdtException {
-    if (key_seed.length != KEY_SEED_SIZE) {
-      throw new LdtException("Key must be " + KEY_SEED_SIZE + " bytes");
-    }
-    if (metadata_key_tag.length != TAG_SIZE) {
-      throw new LdtException("Tag must be " + TAG_SIZE + " bytes");
-    }
-
-    long handle = LdtNpJni.createLdtCipher(key_seed, metadata_key_tag);
-    if (handle == 0) {
-      throw new LdtException("Creating Ldt native resources failed");
-    }
-
-    return new LdtNpCipher(handle);
-  }
-
-  /**
-   * Encode a 2 byte salt as a big-endian char.
-   *
-   * @return a char with b1 in the high bits and b2 in the low bits
-   */
-  public static char saltAsChar(byte b1, byte b2) {
-    // byte widening conversion to int sign-extends
-    int highBits = b1 << 8;
-    int lowBits = b2 & 0xFF;
-    // narrowing conversion truncates to low 16 bits
-    return (char) (highBits | lowBits);
-  }
-
-  /**
-   * Encrypt data in place, XORing bytes derived from the salt into the LDT tweaks.
-   *
-   * @param salt the salt that will be used in the advertisement with this encrypted payload. See
-   *     {@link LdtNpCipher#saltAsChar(byte, byte)} for constructing the char
-   *     representation.
-   * @param data plaintext to encrypt in place: the metadata key followed by the data elements to be
-   *     encrypted. The length must be in [16, 31).
-   * @throws IllegalStateException if this instance has already been closed
-   * @throws IllegalArgumentException if data is the wrong length
-   * @throws LdtException if encryption fails
-   */
-  public void encrypt(char salt, byte[] data) throws LdtException {
-    checkPreconditions(data);
-
-    int res = LdtNpJni.encrypt(ldtHandle, salt, data);
-    if (res < 0) {
-      // TODO is it possible for this to fail if the length is correct?
-      throw new LdtException("Could not encrypt: error code " + res);
-    }
-  }
-
-  /**
-   * Decrypt the data in place, XORing the LDT tweak with the provided bytes.
-   *
-   * @param salt the salt extracted from the advertisement that contained this payload. See {@link
-   *     LdtNpCipher#saltAsChar(byte, byte)} for constructing the char representation.
-   * @param data ciphertext to decrypt in place: the metadata key followed by the data elements to
-   *     be decrypted. The length must be in [16, 31).
-   * @throws IllegalStateException if this instance has already been closed
-   * @throws IllegalArgumentException if data is the wrong length
-   * @throws LdtException if decryption fails
-   */
-  public void decrypt_and_verify(char salt, byte[] data) throws LdtException {
-    checkPreconditions(data);
-
-    int res = LdtNpJni.decrypt_and_verify(ldtHandle, salt, data);
-    if (res < 0) {
-      // TODO is it possible for this to fail if the length is correct?
-      throw new LdtException("Could not decrypt: error code " + res);
-    }
-  }
-
-  private void checkPreconditions(byte[] data) {
-    if (closed) {
-      throw new IllegalStateException("Instance has been closed");
-    }
-    if (data.length < BLOCK_SIZE || data.length >= BLOCK_SIZE * 2) {
-      throw new IllegalArgumentException(
-          "Data must be at least " + BLOCK_SIZE + " and less than " + BLOCK_SIZE * 2 + " bytes");
-    }
-  }
-
-  /**
-   * Releases native resources.
-   *
-   * <p>Once closed, an Ldt instance cannot be used further.
-   */
-  @Override
-  public void close() {
-    if (closed) {
-      return;
-    }
-    closed = true;
-
-    int res = LdtNpJni.closeLdtCipher(ldtHandle);
-    if (res < 0) {
-      throw new RuntimeException("Could not close Ldt: error code " + res);
-    }
-  }
-
-  public static class LdtException extends Exception {
-    LdtException(String message) {
-      super(message);
-    }
-  }
-}
diff --git a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJni.java b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJni.java
index bbb0067..6af362a 100644
--- a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJni.java
+++ b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJni.java
@@ -15,48 +15,90 @@
  */
 package com.google.android.gms.nearby.presence.hazmat;
 
-/** JNI for LDT-XTS-AES128 with the "swap" mix function. */
+import androidx.annotation.IntDef;
+
+
+/** JNI for a Nearby Presence LDT-XTS-AES128 cipher with the "swap" mix function. */
 class LdtNpJni {
 
+  /** Error codes which map to return values on the native side. */
+  @IntDef({DecryptErrorCode.DATA_LEN_ERROR, DecryptErrorCode.JNI_OP_ERROR, DecryptErrorCode.MAC_MISMATCH} )
+  public @interface DecryptErrorCode {
+    int  DATA_LEN_ERROR = -1;
+    int JNI_OP_ERROR = -2;
+    int MAC_MISMATCH = -3;
+  }
+
+  /** Error codes which map to return values on the native side. */
+  @IntDef({EncryptErrorCode.DATA_LEN_ERROR, EncryptErrorCode.JNI_OP_ERROR} )
+  public @interface EncryptErrorCode {
+    int  DATA_LEN_ERROR = -1;
+    int JNI_OP_ERROR = -2;
+  }
+
   static {
     System.loadLibrary("ldt_np_jni");
   }
 
   /**
-   * Create an instance of LDT-XTS-AES-128 using the Swap mix function.
+   * Create a new LDT-XTS-AES128 Encryption cipher using the "swap" mix function.
    *
-   * @param key key bytes, must be 4x AES key size = 64 bytes
-   * @return 0 on error, and any other value for success
+   * @param keySeed is the 32-byte key material from the Nearby Presence credential from which
+   *     the LDT key will be derived.
+   * @return 0 on error, or a non-zero handle on success.
    */
-  static native long createLdtCipher(byte[] key_seed, byte[] metadata_key_hmac_tag);
+  static native long createEncryptionCipher(byte[] keySeed);
 
   /**
-   * Close the native resources for an Ldt instance.
+   * Create a new LDT-XTS-AES128 Decryption cipher using the "swap" mix function.
    *
-   * @param ldtHandle An ldt handle returned from {@link LdtNpJni#createLdtCipher}.
-   * @return 0 on success, <0 for any error
+   * @param keySeed is the 32-byte key material from the Nearby Presence credential from which
+   *     the LDT key will be derived.
+   * @param hmacTag is the hmac auth tag calculated on the metadata key used to verify
+   *     decryption was successful.
+   * @return 0 on error, or a non-zero handle on success.
    */
-  static native int closeLdtCipher(long ldtHandle);
+  static native long createDecryptionCipher(byte[] keySeed, byte[] hmacTag);
 
   /**
-   * Encrypt the data in place.
-   *
-   * @param ldtHandle An ldt handle returned from {@link LdtNpJni#createLdtCipher}.
-   * @param salt big-endian salt to be expanded into bytes XORd into the LDT tweaks
-   * @param data size must be between 16 and 31 bytes
-   * @return 0 on success, -1 if the data size is wrong, or another negative number for any other
-   *     error
+   * Close the native resources for an LdtEncryptCipher instance.
+   * @param ldtEncryptHandle a ldt handle returned from {@link LdtNpJni#createEncryptionCipher}.
    */
-  static native int encrypt(long ldtHandle, char salt, byte[] data);
+  static native void closeEncryptCipher(long ldtEncryptHandle);
 
   /**
-   * Decrypt the data in place using the default LDT tweak padding scheme.
+   * Close the native resources for an LdtDecryptCipher instance.
    *
-   * @param ldtHandle An ldt address returned from {@link LdtNpJni#createLdtCipher}.
-   * @param salt big-endian salt to be expanded into bytes XORd into the LDT tweaks
-   * @param data size must be between 16 and 31 bytes
-   * @return 0 on success, -1 if the data size is wrong, -2 if the calculated hmac
-   *     does not match the provided tag or another negative number for any other error
+   * @param ldtDecryptHandle a ldt handle returned from {@link LdtNpJni#createDecryptionCipher}.
    */
-  static native int decrypt_and_verify(long ldtHandle, char salt, byte[] data);
+  static native void closeDecryptCipher(long ldtDecryptHandle);
+
+  /**
+   * Encrypt a 16-31 byte buffer in-place.
+   *
+   * @param ldtEncryptHandle a ldt encryption handle returned from {@link LdtNpJni#createEncryptionCipher}.
+   * @param salt is the big-endian 2 byte salt that will be used in the Nearby
+   *     Presence advertisement, which will be incorporated into the tweaks LDT uses
+   *     while encrypting.
+   * @param data 16-31 bytes of plaintext data to be encrypted
+   * @return 0 on success, in which case `buffer` will now contain ciphertext or a non-zero
+   *     an error code on failure
+   */
+  @EncryptErrorCode
+  static native int encrypt(long ldtEncryptHandle, char salt, byte[] data);
+
+  /**
+   *  Decrypt a 16-31 byte buffer in-place and verify the plaintext metadata key matches
+   *  this item's MAC, if not the buffer will not be decrypted.
+   *
+   * @param ldtDecryptHandle  a ldt encryption handle returned from {@link LdtNpJni#createDecryptionCipher}.
+   * @param salt is the big-endian 2 byte salt that will be used in the Nearby
+   *     Presence advertisement, which will be incorporated into the tweaks LDT uses
+   *     while encrypting.
+   * @param data 16-31 bytes of ciphertext data to be decrypted
+   * @return 0 on success, in which case `buffer` will now contain ciphertext or a non-zero
+   *     an error code on failure, in which case data will remain unchanged.
+   */
+  @DecryptErrorCode
+  static native int decryptAndVerify(long ldtDecryptHandle, char salt, byte[] data);
 }
diff --git a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/NpLdtCipher.kt b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/NpLdtCipher.kt
new file mode 100644
index 0000000..8f080e8
--- /dev/null
+++ b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/main/java/com/google/android/gms/nearby/presence/hazmat/NpLdtCipher.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.gms.nearby.presence.hazmat
+
+import androidx.annotation.IntDef
+import com.google.android.gms.nearby.presence.hazmat.LdtNpJni.DecryptErrorCode
+import com.google.android.gms.nearby.presence.hazmat.LdtNpJni.EncryptErrorCode
+
+private const val KEY_SEED_SIZE = 32
+private const val TAG_SIZE = 32
+private const val BLOCK_SIZE = 16
+
+// Error return value for create operations
+private const val CREATE_HANDLE_ERROR = 0L
+
+// Status code returned on successful cipher operations
+private const val SUCCESS = 0
+
+/**
+ * A 2 byte salt that will be used in the advertisement with this encrypted payload.
+ */
+class Salt(b1: Byte, b2: Byte) {
+  private val saltBytes: Char
+
+  // Returns the bytes of the salt represented as a 2 byte Char type
+  internal fun getBytesAsChar(): Char {
+    return saltBytes
+  }
+
+  init {
+    // byte widening conversion to int sign-extends
+    val highBits = b1.toInt() shl 8
+    val lowBits = b2.toInt() and 0xFF
+    // narrowing conversion truncates to low 16 bits
+    saltBytes = (highBits or lowBits).toChar()
+  }
+}
+
+/** A runtime exception thrown if something fails during a Ldt jni call*/
+class LdtJniException internal constructor(message: String?) : RuntimeException(message)
+
+/**
+ * A checked exception which occurs if the calculated hmac tag does not match the expected hmac
+ * tag during decrypt operations
+ */
+class MacMismatchException internal constructor(message: String?) : Exception(message)
+
+/**
+ * Create a new LdtEncryptionCipher instance using LDT-XTS-AES128 with the "swap" mix function.
+ *
+ * @constructor Creates a new instance from the provided keySeed
+ * @param keySeed is the key material from the Nearby Presence credential from which
+ *     the LDT key will be derived.
+ * @return an instance configured with the supplied key seed
+ * @throws IllegalArgumentException if the keySeed is the wrong size
+ * @throws LdtJniException if creating the instance fails
+ */
+class LdtEncryptionCipher @Throws(
+  LdtJniException::class,
+  IllegalArgumentException::class
+) constructor(keySeed: ByteArray) :
+  AutoCloseable {
+  @Volatile
+  private var closed = false
+  private val handle: Long
+
+  init {
+    require(keySeed.size == KEY_SEED_SIZE)
+    handle = LdtNpJni.createEncryptionCipher(keySeed)
+    if (handle == CREATE_HANDLE_ERROR) {
+      throw LdtJniException("Creating ldt encryption cipher native resources failed")
+    }
+  }
+
+  /**
+   * Encrypt a 16-31 byte buffer in-place.
+   *
+   * @param salt the salt that will be used in the advertisement with this encrypted payload.
+   * @param data plaintext to encrypt in place: the metadata key followed by the data elements to be
+   * encrypted. The length must be in [16, 31).
+   * @throws IllegalStateException if this instance has already been closed
+   * @throws IllegalArgumentException if data is the wrong length
+   * @throws LdtJniException if encryption fails
+   */
+  @Throws(LdtJniException::class, IllegalArgumentException::class, IllegalStateException::class)
+  fun encrypt(salt: Salt, data: ByteArray) {
+    check(!closed) { "Use after free! This instance has already been closed" }
+    requireValidSizeData(data.size)
+
+    when (val res = LdtNpJni.encrypt(handle, salt.getBytesAsChar(), data)) {
+      EncryptErrorCode.JNI_OP_ERROR -> throw LdtJniException("Error during jni encrypt operation: error code $res");
+      EncryptErrorCode.DATA_LEN_ERROR -> check(false) // this will never happen, lengths checked above
+      else -> check(res == SUCCESS)
+    }
+  }
+
+  /**
+   * Releases native resources.
+   *
+   * <p>Once closed, a Ldt instance cannot be used further.
+   */
+  override fun close() {
+    if (!closed) {
+      closed = true
+      LdtNpJni.closeEncryptCipher(handle)
+    }
+  }
+}
+
+/**
+ * A LdtDecryptionCipher instance which uses LDT-XTS-AES128 with the "swap" mix function.
+ *
+ * @constructor Creates a new instance from the provided keySeed and hmacTag
+ * @param keySeed is the key material from the Nearby Presence credential from which
+ *     the LDT key will be derived.
+ * @param hmacTag is the hmac auth tag calculated on the metadata key used to verify
+ *     decryption was successful.
+ * @return an instance configured with the supplied key seed
+ * @throws IllegalArgumentException if the keySeed is the wrong size
+ * @throws IllegalArgumentException if the hmacTag is the wrong size
+ * @throws LdtJniException if creating the instance fails
+ */
+class LdtDecryptionCipher @Throws(
+  LdtJniException::class,
+  IllegalArgumentException::class
+) constructor(keySeed: ByteArray, hmacTag: ByteArray) : AutoCloseable {
+  @Volatile
+  private var closed = false
+  private val handle: Long
+
+  init {
+    require(keySeed.size == KEY_SEED_SIZE)
+    require(hmacTag.size == TAG_SIZE)
+    handle = LdtNpJni.createDecryptionCipher(keySeed, hmacTag)
+    if (handle == CREATE_HANDLE_ERROR) {
+      throw LdtJniException("Creating ldt decryption cipher native resources failed")
+    }
+  }
+
+  /** Error codes which map to return values on the native side.  */
+  @IntDef(DecryptAndVerifyResultCode.SUCCESS, DecryptAndVerifyResultCode.MAC_MISMATCH)
+  annotation class DecryptAndVerifyResultCode {
+    companion object {
+      const val SUCCESS = 0
+      const val MAC_MISMATCH = -1
+    }
+  }
+
+  /**
+   *  Decrypt a 16-31 byte buffer in-place and verify the plaintext metadata key matches
+   *  this item's MAC, if not the buffer will not be decrypted.
+   *
+   * @param salt the salt extracted from the advertisement that contained this payload.
+   * @param data ciphertext to decrypt in place: the metadata key followed by the data elements to
+   * be decrypted. The length must be in [16, 31).
+   * @return a [DecryptAndVerifyResultCode] indicating of the decrypt operation failed or succeeded.
+   * In the case of a failed decrypt, the provided plaintext will not change.
+   * @throws IllegalStateException if this instance has already been closed
+   * @throws IllegalArgumentException if data is the wrong length
+   * @throws LdtJniException if decryption fails
+   */
+  @Throws(
+    IllegalStateException::class,
+    IllegalArgumentException::class,
+    LdtJniException::class
+  )
+  @DecryptAndVerifyResultCode
+  fun decryptAndVerify(salt: Salt, data: ByteArray): Int {
+    check(!closed) { "Double free! Close should only ever be called once" }
+    requireValidSizeData(data.size)
+
+    when (val res = LdtNpJni.decryptAndVerify(handle, salt.getBytesAsChar(), data)) {
+      DecryptErrorCode.MAC_MISMATCH -> return DecryptAndVerifyResultCode.MAC_MISMATCH
+      DecryptErrorCode.DATA_LEN_ERROR -> check(false); // This condition is impossible, we validated data length above
+      DecryptErrorCode.JNI_OP_ERROR -> throw LdtJniException("Error occurred during jni encrypt operation")
+      else -> check(res == SUCCESS)
+    }
+    return DecryptAndVerifyResultCode.SUCCESS
+  }
+
+  /**
+   * Releases native resources.
+   *
+   * <p>Once closed, a Ldt instance cannot be used further.
+   */
+  override fun close() {
+    if (!closed) {
+      closed = true
+      LdtNpJni.closeEncryptCipher(handle)
+    }
+  }
+}
+
+private fun requireValidSizeData(size: Int) {
+  require(size >= BLOCK_SIZE && size < BLOCK_SIZE * 2) { "Invalid size data: $size" }
+}
+
diff --git a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/test/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJniTests.kt b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/test/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJniTests.kt
index dc861c6..07d29a3 100644
--- a/nearby/presence/ldt_np_jni/java/LdtNpJni/src/test/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJniTests.kt
+++ b/nearby/presence/ldt_np_jni/java/LdtNpJni/src/test/java/com/google/android/gms/nearby/presence/hazmat/LdtNpJniTests.kt
@@ -16,27 +16,143 @@
 
 package com.google.android.gms.nearby.presence.hazmat
 
-import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertArrayEquals
+import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+const val KEY_SEED = "CCDB2489E9FCAC42B39348B8941ED19A1D360E75E098C8C15E6B1CC2B620CD39"
+const val HMAC_TAG = "DFB90A1F9B1FE28D18BBCCA52240B5CC2CCB5F8D5289A3CB64EB3541CA614BB4"
+const val PLAINTEXT = "CD683FE1A1D1F846543D0A13D4AEA40040C8D67B"
+const val SALT_BYTES = "32EE"
+const val EXPECTED_CIPHER_TEXT = "04344411F1E57C841FE0F7150636BC782455059A"
 
 class LdtNpJniTests {
   @Test
-  fun ldtRoundTripTest() {
+  fun roundTripTest() {
     // Data taken from first test case in ldt_np_adv/resources/test/np_adv_test_vectors.json
-    val key_seed = "CCDB2489E9FCAC42B39348B8941ED19A1D360E75E098C8C15E6B1CC2B620CD39".decodeHex()
-    val hmac_tag = "DFB90A1F9B1FE28D18BBCCA52240B5CC2CCB5F8D5289A3CB64EB3541CA614BB4".decodeHex()
-    val plaintext = "CD683FE1A1D1F846543D0A13D4AEA40040C8D67B".decodeHex()
-    val salt_bytes = "32EE".decodeHex()
-    val expected_ciphertext = "04344411F1E57C841FE0F7150636BC782455059A".decodeHex()
-    val salt = LdtNpCipher.saltAsChar(salt_bytes[0], salt_bytes[1])
+    val keySeed = KEY_SEED.decodeHex()
+    val hmacTag = HMAC_TAG.decodeHex()
+    val plaintext = PLAINTEXT.decodeHex()
+    val saltBytes = SALT_BYTES.decodeHex()
+    val expectedCiphertext = EXPECTED_CIPHER_TEXT.decodeHex()
+    val salt = Salt(saltBytes[0], saltBytes[1])
 
     val data = plaintext.copyOf()
-    val LdtCipher = LdtNpCipher.fromKey(key_seed, hmac_tag)
-    LdtCipher.encrypt(salt, data)
-    Assertions.assertArrayEquals(expected_ciphertext, data)
+    val encryptionCipher = LdtEncryptionCipher(keySeed)
+    encryptionCipher.encrypt(salt, data)
+    assertArrayEquals(expectedCiphertext, data)
+    encryptionCipher.close()
 
-    LdtCipher.decrypt_and_verify(salt, data)
-    Assertions.assertArrayEquals(plaintext, data)
+
+    val decryptionCipher = LdtDecryptionCipher(keySeed, hmacTag)
+    val result = decryptionCipher.decryptAndVerify(salt, data)
+    assertEquals(LdtDecryptionCipher.DecryptAndVerifyResultCode.SUCCESS, result)
+    assertArrayEquals(plaintext, data)
+    decryptionCipher.close()
+  }
+
+  @Test
+  fun createEncryptionCipherInvalidLength() {
+    assertThrows<IllegalArgumentException> {
+      val keySeed = ByteArray(31)
+      LdtEncryptionCipher(keySeed)
+    }
+
+    assertThrows<IllegalArgumentException> {
+      val keySeed = ByteArray(33)
+      LdtEncryptionCipher(keySeed)
+    }
+  }
+
+  @Test
+  fun encryptInvalidLengthData() {
+    val keySeed = KEY_SEED.decodeHex()
+    val cipher = LdtEncryptionCipher(keySeed)
+    assertThrows<IllegalArgumentException> {
+      var data = ByteArray(15)
+      cipher.encrypt(Salt(0x0, 0x0), data)
+    }
+    assertThrows<IllegalArgumentException> {
+      var data = ByteArray(32)
+      cipher.encrypt(Salt(0x0, 0x0), data)
+    }
+  }
+
+  @Test
+  fun encryptUseAfterClose() {
+    val keySeed = KEY_SEED.decodeHex()
+    val cipher = LdtEncryptionCipher(keySeed)
+    val data = ByteArray(20)
+    cipher.close()
+    assertThrows<IllegalStateException> { cipher.encrypt(Salt(0x0, 0x0), data) }
+  }
+
+  @Test
+  fun createDecryptionCipherInvalidLengths() {
+    assertThrows<IllegalArgumentException> {
+      val keySeed = ByteArray(31)
+      val hmacTag = ByteArray(31)
+      LdtDecryptionCipher(keySeed, hmacTag)
+    }
+    assertThrows<IllegalArgumentException> {
+      val keySeed = ByteArray(33)
+      val hmacTag = ByteArray(33)
+      LdtDecryptionCipher(keySeed, hmacTag)
+    }
+    assertThrows<IllegalArgumentException> {
+      val keySeed = ByteArray(32)
+      val hmacTag = ByteArray(33)
+      LdtDecryptionCipher(keySeed, hmacTag)
+    }
+    assertThrows<IllegalArgumentException> {
+      val keySeed = ByteArray(33)
+      val hmacTag = ByteArray(32)
+      LdtDecryptionCipher(keySeed, hmacTag)
+    }
+  }
+
+  @Test
+  fun decryptInvalidLengthData() {
+    val keySeed = KEY_SEED.decodeHex()
+    val hmacTag = HMAC_TAG.decodeHex()
+    val cipher = LdtDecryptionCipher(keySeed, hmacTag)
+    assertThrows<IllegalArgumentException> {
+      var data = ByteArray(15)
+      cipher.decryptAndVerify(Salt(0x0, 0x0), data)
+    }
+    assertThrows<IllegalArgumentException> {
+      var data = ByteArray(32)
+      cipher.decryptAndVerify(Salt(0x0, 0x0), data)
+    }
+  }
+
+  @Test
+  fun decryptMacMismatch() {
+    val keySeed = KEY_SEED.decodeHex()
+    val hmacTag = HMAC_TAG.decodeHex()
+
+    // alter first byte in the hmac tag
+    hmacTag[0] = 0x00
+    val cipher = LdtDecryptionCipher(keySeed, hmacTag)
+
+    val cipherText = EXPECTED_CIPHER_TEXT.decodeHex()
+    val saltBytes = SALT_BYTES.decodeHex()
+    val salt = Salt(saltBytes[0], saltBytes[1])
+
+    val result = cipher.decryptAndVerify(salt, cipherText);
+    assertEquals(LdtDecryptionCipher.DecryptAndVerifyResultCode.MAC_MISMATCH, result)
+  }
+
+  @Test
+  fun decryptUseAfterClose() {
+    val keySeed = KEY_SEED.decodeHex()
+    val hmacTag = HMAC_TAG.decodeHex()
+    val cipher = LdtDecryptionCipher(keySeed, hmacTag)
+    cipher.close()
+
+    val data = ByteArray(20)
+    assertThrows<IllegalStateException> { cipher.decryptAndVerify(Salt(0x0, 0x0), data) }
   }
 }
 
diff --git a/nearby/presence/ldt_np_jni/src/lib.rs b/nearby/presence/ldt_np_jni/src/lib.rs
index 8f7958e..538a44e 100644
--- a/nearby/presence/ldt_np_jni/src/lib.rs
+++ b/nearby/presence/ldt_np_jni/src/lib.rs
@@ -20,6 +20,8 @@
 //! - <https://www.iitk.ac.in/esc101/05Aug/tutorial/native1.1/index.html>
 //! - <https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html>
 
+// We are not actually no_std because the jni crate is pulling it in, but at least this enforces
+// that this lib isn't using anything from the std lib
 #![no_std]
 #![deny(missing_docs)]
 
@@ -28,10 +30,9 @@
 
 use alloc::boxed::Box;
 
-use jni::objects::JByteArray;
 use jni::{
-    objects::JClass,
-    sys::{jbyte, jbyteArray, jchar, jint, jlong},
+    objects::{JByteArray, JClass},
+    sys::{jbyte, jchar, jint, jlong},
     JNIEnv,
 };
 
@@ -45,149 +46,199 @@
 const MIN_DATA_LEN: usize = crypto_provider::aes::BLOCK_SIZE;
 const MAX_DATA_LEN: usize = crypto_provider::aes::BLOCK_SIZE * 2 - 1;
 
-/// Error return value for creating handles
+/// Required size constraints of input parameters
+const KEY_SEED_SIZE: usize = 32;
+const TAG_SIZE: usize = 32;
+
+/// Error return value for create operations
 const CREATE_ERROR: jlong = 0;
 
-// TODO: don't allow panics to cross FFI boundary
-// TODO: JNI null checks? (only if jni crate isn't doing them already).
+/// Status code returned on successful cipher operations
+const SUCCESS: jint = 0;
 
-// TODO: split this into separate APIs for encrypt and decrypt
-struct Ldt {
-    ldt_enc: LdtEncrypterXtsAes128<CryptoProviderImpl>,
-    ldt_dec: LdtNpAdvDecrypterXtsAes128<CryptoProviderImpl>,
-}
+type LdtAdvDecrypter = LdtNpAdvDecrypterXtsAes128<CryptoProviderImpl>;
+type LdtAdvEncrypter = LdtEncrypterXtsAes128<CryptoProviderImpl>;
 
-/// Create an LDT cipher.
+/// Marker trait to ensure above types are thread safe
+trait JniThreadSafe: Send + Sync {}
+
+impl JniThreadSafe for LdtAdvDecrypter {}
+
+impl JniThreadSafe for LdtAdvEncrypter {}
+
+/// Create a LDT Encryption cipher.
 ///
-/// Returns 0 for error, or the pointer as a jlong/i64.
-/// Safety: We know the key pointer is safe as it is coming directly from the JVM.
+/// Returns 0 on failure, or the non-zero handle as a jlong/i64 on success.
 #[no_mangle]
-#[allow(clippy::not_unsafe_ptr_arg_deref)]
-pub extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_createLdtCipher(
+extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_createEncryptionCipher(
     env: JNIEnv,
     _class: JClass,
-    key_seed: jbyteArray,
-    metadata_key_hmac_tag: jbyteArray,
+    java_key_seed: JByteArray,
 ) -> jlong {
-    env.get_array_length(unsafe { &JByteArray::from_raw(key_seed) })
-        .map_err(|_| CREATE_ERROR)
-        // check length
-        .and_then(|len| if len as usize != 32 { Err(CREATE_ERROR) } else { Ok(len) })
-        // extract u8 array
-        .and_then(|len| {
-            let mut jbyte_buf = [jbyte::default(); 32];
-            env.get_byte_array_region(
-                unsafe { &JByteArray::from_raw(key_seed) },
-                0,
-                &mut jbyte_buf[..],
-            )
-            .map_err(|_| CREATE_ERROR)
-            .map(|_| (len, jbyte_array_to_u8_array(jbyte_buf)))
-        })
-        // initialize ldt -- we already know the key is the right length
-        .and_then(|(_len, key_seed_buf)| {
-            let hkdf_key_seed = NpKeySeedHkdf::new(&key_seed_buf);
-            let ldt_enc = ldt_np_adv::LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(
-                &hkdf_key_seed.legacy_ldt_key(),
-            );
+    create_map_to_error(|| {
+        let key_seed =
+            env.convert_byte_array(&java_key_seed).map_err(|_| CREATE_ERROR).and_then(|seed| {
+                if seed.len() != KEY_SEED_SIZE {
+                    Err(CREATE_ERROR)
+                } else {
+                    Ok(seed)
+                }
+            })?;
 
-            let mut tag_buff = [jbyte::default(); 32];
-            let tag = env
-                .get_byte_array_region(
-                    unsafe { &JByteArray::from_raw(metadata_key_hmac_tag) },
-                    0,
-                    &mut tag_buff[..],
-                )
-                .map_err(|_| CREATE_ERROR)
-                .map(|_| jbyte_array_to_u8_array(tag_buff))
-                .unwrap();
-            // TODO: Error handling
+        let hkdf_key_seed = NpKeySeedHkdf::<CryptoProviderImpl>::new(
+            key_seed.as_slice().try_into().expect("Length is checked above"),
+        );
 
-            let ldt_dec = ldt_np_adv::build_np_adv_decrypter_from_key_seed::<CryptoProviderImpl>(
-                &hkdf_key_seed,
-                tag,
-            );
-            box_to_handle(Ldt { ldt_enc, ldt_dec }).map_err(|_| CREATE_ERROR)
-        })
-        .unwrap_or_else(|e| e)
+        let cipher = LdtAdvEncrypter::new(&hkdf_key_seed.legacy_ldt_key());
+        box_to_handle(cipher).map_err(|_| CREATE_ERROR)
+    })
 }
 
-/// Close an LDT cipher.
+/// Create a LDT Decryption cipher.
+///
+/// Returns 0 on failure, or the non-zero handle as a jlong/i64 on success.
 #[no_mangle]
-pub extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_closeLdtCipher(
+extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_createDecryptionCipher(
+    env: JNIEnv,
+    _class: JClass,
+    java_key_seed: JByteArray,
+    java_hmac_tag: JByteArray,
+) -> jlong {
+    create_map_to_error(|| {
+        let key_seed =
+            env.convert_byte_array(&java_key_seed).map_err(|_| CREATE_ERROR).and_then(|seed| {
+                if seed.len() != KEY_SEED_SIZE {
+                    Err(CREATE_ERROR)
+                } else {
+                    Ok(seed)
+                }
+            })?;
+        let hmac_tag =
+            env.convert_byte_array(&java_hmac_tag).map_err(|_| CREATE_ERROR).and_then(|tag| {
+                if tag.len() != TAG_SIZE {
+                    Err(CREATE_ERROR)
+                } else {
+                    Ok(tag)
+                }
+            })?;
+        let hkdf_key_seed = NpKeySeedHkdf::<CryptoProviderImpl>::new(
+            key_seed.as_slice().try_into().expect("Length is checked above"),
+        );
+
+        let cipher = ldt_np_adv::build_np_adv_decrypter_from_key_seed::<CryptoProviderImpl>(
+            &hkdf_key_seed,
+            hmac_tag.as_slice().try_into().expect("Length is checked above"),
+        );
+        box_to_handle(cipher).map_err(|_| CREATE_ERROR)
+    })
+}
+
+fn create_map_to_error<F: Fn() -> Result<jlong, jlong>>(f: F) -> jlong {
+    f().unwrap_or_else(|e| e)
+}
+
+/// Close an LDT Encryption Cipher
+#[no_mangle]
+extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_closeEncryptCipher(
     _env: JNIEnv,
     _class: JClass,
-    ldt_handle: jlong,
-) -> jint {
+    handle: jlong,
+) {
     // create the box, let it be dropped
-    let _ = boxed_from_handle::<Ldt>(ldt_handle);
-    // success -- are there any meaningful error condtions we can even detect?
-    0
+    let _ = boxed_from_handle::<LdtAdvEncrypter>(handle);
+}
+
+/// Close an LDT Decryption Cipher
+#[no_mangle]
+extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_closeDecryptCipher(
+    _env: JNIEnv,
+    _class: JClass,
+    handle: jlong,
+) {
+    // create the box, let it be dropped
+    let _ = boxed_from_handle::<LdtAdvDecrypter>(handle);
 }
 
 /// Encrypt a buffer in place.
-/// Safety: We know the data jArray pointer is safe because it is coming directly from the JVM.
 #[no_mangle]
-#[allow(clippy::not_unsafe_ptr_arg_deref)]
-pub extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_encrypt(
+extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_encrypt(
     env: JNIEnv,
     _class: JClass,
-    ldt_handle: jlong,
+    handle: jlong,
     salt: jchar,
-    data: jbyteArray,
+    data: JByteArray,
 ) -> jint {
-    jbyte_cipher_data_as_u8_array(&env, data)
-        .and_then(|(len, mut data_u8)| {
-            with_handle::<Ldt, _, _>(ldt_handle, |ldt| {
-                ldt.ldt_enc.encrypt(&mut data_u8[..len], &expand_np_salt_to_padder(salt)).map_err(
-                    |err| match err {
-                        ldt::LdtError::InvalidLength(_) => CipherOpError::DataLen,
-                    },
-                )?;
-                env.set_byte_array_region(
-                    unsafe { &JByteArray::from_raw(data) },
-                    0,
-                    &u8_slice_to_jbyte_array(data_u8)[..len],
-                )
-                .map_err(|_| CipherOpError::JniOp)
-                .map(|_| 0) // success
-            })
+    map_to_error_code(|| {
+        let mut buffer =
+            env.convert_byte_array(&data).map_err(|_| EncryptError::JniOp).and_then(|data| {
+                if !(MIN_DATA_LEN..=MAX_DATA_LEN).contains(&data.len()) {
+                    Err(EncryptError::DataLen)
+                } else {
+                    Ok(data)
+                }
+            })?;
+
+        with_handle::<LdtAdvEncrypter, _, _>(handle, |cipher| {
+            cipher.encrypt(buffer.as_mut_slice(), &expand_np_salt_to_padder(salt)).map_err(
+                |err| match err {
+                    ldt::LdtError::InvalidLength(_) => EncryptError::DataLen,
+                },
+            )?;
+
+            // Avoid a copy since transmuting from a &[u8] to a &[i8] is safe
+            // Safety:
+            // - u8 and jbyte/i8 are the same size have the same alignment
+            let jbyte_buffer = bytes_to_jbytes(buffer.as_slice());
+
+            env.set_byte_array_region(&data, 0, jbyte_buffer)
+                .map_err(|_| EncryptError::JniOp)
+                .map(|_| SUCCESS)
         })
-        .unwrap_or_else(|e| e.to_jni_error_code())
+    })
 }
 
 /// Decrypt a buffer in place.
 /// Safety: We know the data pointer is safe because it is coming directly from the JVM.
 #[no_mangle]
-#[allow(clippy::not_unsafe_ptr_arg_deref)]
-pub extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_decrypt_1and_1verify(
+extern "system" fn Java_com_google_android_gms_nearby_presence_hazmat_LdtNpJni_decryptAndVerify(
     env: JNIEnv,
     _class: JClass,
-    ldt_handle: jlong,
+    handle: jlong,
     salt: jchar,
-    data: jbyteArray,
+    data: JByteArray,
 ) -> jint {
-    jbyte_cipher_data_as_u8_array(&env, data)
-        .and_then(|(len, mut data_u8)| {
-            with_handle::<Ldt, _, _>(ldt_handle, |ldt| {
-                let result = ldt
-                    .ldt_dec
-                    .decrypt_and_verify(&data_u8[..len], &expand_np_salt_to_padder(salt))
-                    .map_err(|err| match err {
-                        LdtAdvDecryptError::InvalidLength(_) => CipherOpError::DataLen,
-                        LdtAdvDecryptError::MacMismatch => CipherOpError::MacMisMatch,
-                    })?;
-                data_u8[..result.len()].copy_from_slice(result.as_slice());
-                env.set_byte_array_region(
-                    unsafe { &JByteArray::from_raw(data) },
-                    0,
-                    &u8_slice_to_jbyte_array(data_u8)[..len],
-                )
-                .map_err(|_| CipherOpError::JniOp)
-                .map(|_| 0) // success
-            })
+    map_to_error_code(|| {
+        let mut buffer =
+            env.convert_byte_array(&data).map_err(|_| DecryptError::JniOp).and_then(|data| {
+                if !(MIN_DATA_LEN..=MAX_DATA_LEN).contains(&data.len()) {
+                    Err(DecryptError::DataLen)
+                } else {
+                    Ok(data)
+                }
+            })?;
+
+        with_handle::<LdtAdvDecrypter, _, _>(handle, |cipher| {
+            let result = cipher
+                .decrypt_and_verify(buffer.as_mut_slice(), &expand_np_salt_to_padder(salt))
+                .map_err(|err| match err {
+                    LdtAdvDecryptError::InvalidLength(_) => DecryptError::DataLen,
+                    LdtAdvDecryptError::MacMismatch => DecryptError::MacMisMatch,
+                })?;
+
+            let jbyte_buffer = bytes_to_jbytes(result.as_slice());
+
+            env.set_byte_array_region(&data, 0, jbyte_buffer)
+                .map_err(|_| DecryptError::JniOp)
+                .map(|_| SUCCESS)
         })
-        .unwrap_or_else(|e| e.to_jni_error_code())
+    })
+}
+
+/// A zero-copy conversion from a u8 slice to a jbyte slice
+fn bytes_to_jbytes(bytes: &[u8]) -> &[jbyte] {
+    // Safety:
+    // - u8 and jbyte/i8 are the same size have the same alignment
+    unsafe { alloc::slice::from_raw_parts(bytes.as_ptr() as *const jbyte, bytes.len()) }
 }
 
 /// Reconstruct a `Box<T>` from `handle`, and invoke `f` with the resulting `&T`.
@@ -202,7 +253,6 @@
 
     // don't consume the box -- need to keep the handle alive
     Box::leak(boxed);
-
     ret
 }
 
@@ -243,52 +293,6 @@
         .map(|ptr_64: u64| ptr_64 as jlong)
 }
 
-/// Extract data suitable for Ldt128 cipher ops from a JNI jbyteArray.
-///
-/// Returns `(data len in buffer, buffer)`, or `Err` if any JNI ops fail.
-fn jbyte_cipher_data_as_u8_array(
-    env: &JNIEnv,
-    cipher_data: jbyteArray,
-) -> Result<(usize, [u8; MAX_DATA_LEN]), CipherOpError> {
-    let data_len = env
-        .get_array_length(unsafe { &JByteArray::from_raw(cipher_data) })
-        .map_err(|_| CipherOpError::JniOp)? as usize;
-    if !(MIN_DATA_LEN..=MAX_DATA_LEN).contains(&data_len) {
-        return Err(CipherOpError::DataLen);
-    }
-
-    let mut buf = [jbyte::default(); MAX_DATA_LEN];
-    env.get_byte_array_region(
-        unsafe { &JByteArray::from_raw(cipher_data) },
-        0,
-        &mut buf[0..data_len],
-    )
-    .map_err(|_| CipherOpError::JniOp)?;
-
-    Ok((data_len, jbyte_array_to_u8_array(buf)))
-}
-
-/// Convert a jbyte array to a u8 array
-fn jbyte_array_to_u8_array<const N: usize>(src: [jbyte; N]) -> [u8; N] {
-    let mut dest = [0_u8; N];
-    for i in 0..N {
-        // numeric cast doesn't alter bits, which is what we want
-        // https://doc.rust-lang.org/reference/expressions/operator-expr.html#semantics
-        dest[i] = src[i] as u8;
-    }
-    dest
-}
-
-fn u8_slice_to_jbyte_array<const N: usize>(src: [u8; N]) -> [jbyte; N] {
-    let mut dest = [0_i8; N];
-    for i in 0..N {
-        // numeric cast doesn't alter bits, which is what we want
-        // https://doc.rust-lang.org/reference/expressions/operator-expr.html#semantics
-        dest[i] = src[i] as jbyte;
-    }
-    dest
-}
-
 /// Expand the NP salt to the size needed to be an LDT XorPadder.
 ///
 /// Returns a XorPadder containing the HKDF of the salt.
@@ -297,23 +301,48 @@
     ldt_np_adv::salt_padder::<16, CryptoProviderImpl>(salt_bytes.into())
 }
 
+fn map_to_error_code<E: JniError, F: Fn() -> Result<jint, E>>(f: F) -> jint {
+    f().unwrap_or_else(|e| e.to_jni_error_code())
+}
+
+trait JniError {
+    fn to_jni_error_code(&self) -> jint;
+}
+
 #[derive(Debug)]
-enum CipherOpError {
-    /// The mac did not match the provided tag
-    MacMisMatch,
+enum EncryptError {
     /// Data is the wrong length
     DataLen,
     /// JNI op failed
     JniOp,
 }
 
-impl CipherOpError {
+impl JniError for EncryptError {
+    fn to_jni_error_code(&self) -> jint {
+        match self {
+            Self::DataLen => -1,
+            Self::JniOp => -2,
+        }
+    }
+}
+
+#[derive(Debug)]
+enum DecryptError {
+    /// Data is the wrong length
+    DataLen,
+    /// The mac did not match the provided tag
+    MacMisMatch,
+    /// JNI op failed
+    JniOp,
+}
+
+impl JniError for DecryptError {
     /// Returns an error code suitable for returning from Ldt encrypt/decrypt JNI calls.
     fn to_jni_error_code(&self) -> jint {
         match self {
-            CipherOpError::DataLen => -1,
-            CipherOpError::MacMisMatch => -2,
-            CipherOpError::JniOp => -3,
+            Self::DataLen => -1,
+            Self::JniOp => -2,
+            Self::MacMisMatch => -3,
         }
     }
 }
diff --git a/nearby/presence/np_adv/Cargo.toml b/nearby/presence/np_adv/Cargo.toml
index 4c8aff5..5123737 100644
--- a/nearby/presence/np_adv/Cargo.toml
+++ b/nearby/presence/np_adv/Cargo.toml
@@ -14,29 +14,43 @@
 crypto_provider.workspace = true
 strum.workspace = true
 strum_macros.workspace = true
-nom = { version = "7.1.1", features = ["alloc"] }
+nom = { version = "7.1.3", default-features = false, features = ["alloc"] }
 lazy_static.workspace = true
 sink.workspace = true
 tinyvec.workspace = true
-rand.workspace = true
 
 [features]
 default = []
 devtools = []
 testing = []
+alloc = ["crypto_provider/alloc"]
 
 [dev-dependencies]
 hex.workspace = true
+rand.workspace = true
 rand_ext = { path = "../rand_ext" }
 init_with = "1.1.0"
-serde_json.workspace = true
+serde_json = {workspace = true, features = ["std"]}
+serde.workspace = true
 anyhow.workspace = true
 test_helper = { path = "../test_helper" }
 criterion.workspace = true
-crypto_provider_default = {workspace = true, features = ["std", "rustcrypto"]}
-np_ed25519 = {workspace = true, features = ["std"]}
-sink = {workspace = true, features = ["std"]}
+crypto_provider_default = { workspace = true, features = ["std", "rustcrypto"] }
+np_ed25519 = { workspace = true, features = ["std"] }
+rmp-serde = "1.1.2"
+sink = { workspace = true, features = ["std"] }
 
 [[bench]]
 name = "deser_adv"
 harness = false
+required-features = ["alloc", "devtools"]
+
+[[test]]
+name = "examples_v0"
+path = "tests/examples_v0.rs"
+required-features = ["alloc"]
+
+[[test]]
+name = "examples_v1"
+path = "tests/examples_v1.rs"
+required-features = ["alloc"]
diff --git a/nearby/presence/np_adv/benches/deser_adv.rs b/nearby/presence/np_adv/benches/deser_adv.rs
index 9699ebf..e998047 100644
--- a/nearby/presence/np_adv/benches/deser_adv.rs
+++ b/nearby/presence/np_adv/benches/deser_adv.rs
@@ -16,26 +16,29 @@
 use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion};
 use crypto_provider::{CryptoProvider, CryptoRng};
 use crypto_provider_default::CryptoProviderImpl;
-use ldt_np_adv::{LdtEncrypterXtsAes128, LegacySalt};
+use ldt_np_adv::LegacySalt;
 
+use np_adv::deserialization_arena;
+use np_adv::extended::serialize::AdvertisementType;
 use np_adv::{
-    credential::{simple::*, source::*, v0::*, v1::*},
+    credential::{book::*, v0::*, v1::*, *},
     de_type::EncryptedIdentityDataElementType,
-    deserialize_advertisement, deserialize_v0_advertisement, deserialize_v1_advertisement,
+    deserialize_advertisement,
     extended::{
         data_elements::{GenericDataElement, TxPowerDataElement},
         deserialize::VerificationMode,
         serialize::{
-            AdvBuilder as ExtendedAdvBuilder, MicEncrypted, SectionBuilder, SectionIdentity,
-            SignedEncrypted,
+            AdvBuilder as ExtendedAdvBuilder, MicEncryptedSectionEncoder, PublicSectionEncoder,
+            SectionBuilder, SectionEncoder, SignedEncryptedSectionEncoder,
         },
     },
     legacy::{
         actions::{ActionBits, ActionsDataElement},
         serialize::{AdvBuilder as LegacyAdvBuilder, LdtIdentity},
+        ShortMetadataKey,
     },
     shared_data::{ContextSyncSeqNum, TxPower},
-    PublicIdentity,
+    MetadataKey, PublicIdentity,
 };
 use rand::{Rng as _, SeedableRng as _};
 use strum::IntoEnumIterator;
@@ -58,21 +61,22 @@
                                 .map(|_| V1Identity::random(&mut crypto_rng))
                                 .collect::<Vec<_>>();
 
-                            let mut adv_builder = ExtendedAdvBuilder::new();
+                            let mut adv_builder = ExtendedAdvBuilder::new(AdvertisementType::Encrypted);
 
                             // take the first n identities, one section per identity
                             for identity in identities.iter().take(num_sections) {
-                                let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(
-                                    &identity.key_seed,
+                                let broadcast_cm = SimpleSignedBroadcastCryptoMaterial::new(
+                                    identity.key_seed,
+                                    identity.extended_metadata_key,
+                                    identity.key_pair.private_key(),
                                 );
                                 match identity_type {
                                     VerificationMode::Mic => {
                                         let mut sb = adv_builder
-                                            .section_builder(MicEncrypted::new_random_salt(
+                                            .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                                                 &mut crypto_rng,
                                                 EncryptedIdentityDataElementType::Private,
-                                                &identity.extended_metadata_key,
-                                                &hkdf,
+                                                &broadcast_cm,
                                             ))
                                             .unwrap();
 
@@ -81,12 +85,10 @@
                                     }
                                     VerificationMode::Signature => {
                                         let mut sb = adv_builder
-                                            .section_builder(SignedEncrypted::new_random_salt(
+                                            .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                                                 &mut crypto_rng,
                                                 EncryptedIdentityDataElementType::Private,
-                                                &identity.extended_metadata_key,
-                                                &identity.key_pair,
-                                                &hkdf,
+                                                &broadcast_cm,
                                             ))
                                             .unwrap();
 
@@ -98,22 +100,11 @@
 
                             let adv = adv_builder.into_advertisement();
 
-                            match crypto_type {
-                                CryptoMaterialType::MinFootprint => run_with_v1_creds::<
-                                    MinimumFootprintV1CryptoMaterial,
-                                    CryptoProviderImpl,
-                                >(
-                                    b, identities, adv.as_slice()
-                                ),
-                                CryptoMaterialType::Precalculated => {
-                                    run_with_v1_creds::<
-                                        PrecalculatedV1CryptoMaterial,
-                                        CryptoProviderImpl,
-                                    >(
-                                        b, identities, adv.as_slice()
-                                    )
-                                }
-                            }
+                            run_with_v1_creds::<
+                                CryptoProviderImpl
+                            >(
+                                b, crypto_type, identities, adv.as_slice()
+                            )
                         },
                     );
                 }
@@ -123,31 +114,23 @@
 }
 
 pub fn deser_adv_v1_plaintext(c: &mut Criterion) {
-    let _rng = rand::rngs::StdRng::from_entropy();
+    c.bench_function("Deser V1 plaintext: sections=1", |b| {
+        let mut adv_builder = ExtendedAdvBuilder::new(AdvertisementType::Plaintext);
 
-    for &num_sections in &[1, 2, 5, 8] {
-        // measure worst-case performance -- the correct identities will be the last
-        // num_sections of the identities to be tried
-        c.bench_function(&format!("Deser V1 plaintext: sections={num_sections}"), |b| {
-            let mut adv_builder = ExtendedAdvBuilder::new();
+        let mut sb = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
-            // take the first n identities, one section per identity
-            for _ in 0..num_sections {
-                let mut sb = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+        add_des(&mut sb);
+        sb.add_to_advertisement();
 
-                add_des(&mut sb);
-                sb.add_to_advertisement();
-            }
+        let adv = adv_builder.into_advertisement();
 
-            let adv = adv_builder.into_advertisement();
-
-            run_with_v1_creds::<MinimumFootprintV1CryptoMaterial, CryptoProviderImpl>(
-                b,
-                vec![],
-                adv.as_slice(),
-            )
-        });
-    }
+        run_with_v1_creds::<CryptoProviderImpl>(
+            b,
+            CryptoMaterialType::MinFootprint,
+            vec![],
+            adv.as_slice(),
+        )
+    });
 }
 
 pub fn deser_adv_v0_encrypted(c: &mut Criterion) {
@@ -164,17 +147,17 @@
                         .collect::<Vec<_>>();
 
                     let identity = &identities[0];
-                    let hkdf =
-                        np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&identity.key_seed);
+
+                    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(
+                        identity.key_seed,
+                        identity.legacy_metadata_key,
+                    );
 
                     let mut adv_builder =
                         LegacyAdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
                             EncryptedIdentityDataElementType::Private,
                             LegacySalt::from(rng.gen::<[u8; 2]>()),
-                            identity.legacy_metadata_key,
-                            LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(
-                                &hkdf.legacy_ldt_key(),
-                            ),
+                            &broadcast_cm,
                         ));
 
                     let mut action_bits = ActionBits::default();
@@ -183,46 +166,31 @@
 
                     let adv = adv_builder.into_advertisement().unwrap();
 
-                    match crypto_type {
-                        CryptoMaterialType::MinFootprint => run_with_v0_creds::<
-                            MinimumFootprintV0CryptoMaterial,
-                            CryptoProviderImpl,
-                        >(
-                            b, identities, adv.as_slice()
-                        ),
-                        CryptoMaterialType::Precalculated => run_with_v0_creds::<
-                            PrecalculatedV0CryptoMaterial,
-                            CryptoProviderImpl,
-                        >(
-                            b, identities, adv.as_slice()
-                        ),
-                    }
+                    run_with_v0_creds::<CryptoProviderImpl>(
+                        b,
+                        crypto_type,
+                        identities,
+                        adv.as_slice(),
+                    )
                 },
             );
         }
     }
 }
 
-type DefaultV0Credential = np_adv::credential::simple::SimpleV0Credential<
-    np_adv::credential::v0::MinimumFootprintV0CryptoMaterial,
-    (),
->;
-type DefaultV1Credential = np_adv::credential::simple::SimpleV1Credential<
-    np_adv::credential::v1::MinimumFootprintV1CryptoMaterial,
-    (),
->;
-type DefaultBothCredentialSource =
-    np_adv::credential::source::OwnedBothCredentialSource<DefaultV0Credential, DefaultV1Credential>;
-
 pub fn deser_adv_v0_plaintext(c: &mut Criterion) {
-    let mut adv_builder = LegacyAdvBuilder::new(PublicIdentity::default());
+    let mut adv_builder = LegacyAdvBuilder::new(PublicIdentity);
 
     let mut action_bits = ActionBits::default();
     action_bits.set_action(ContextSyncSeqNum::try_from(3).unwrap());
     adv_builder.add_data_element(ActionsDataElement::from(action_bits)).unwrap();
     let adv = adv_builder.into_advertisement().unwrap();
 
-    let cred_source: DefaultBothCredentialSource = DefaultBothCredentialSource::new_empty();
+    let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
 
     for &num_advs in &[1, 10, 100, 1000] {
         c.bench_function(
@@ -231,9 +199,10 @@
                 b.iter(|| {
                     for _ in 0..num_advs {
                         black_box(
-                            deserialize_advertisement::<_, _, _, _, CryptoProviderImpl>(
+                            deserialize_advertisement::<_, CryptoProviderImpl>(
+                                deserialization_arena!(),
                                 black_box(adv.as_slice()),
-                                black_box(&cred_source),
+                                black_box(&cred_book),
                             )
                             .expect("Should succeed"),
                         );
@@ -246,22 +215,20 @@
 
 /// Benchmark decrypting a V0 advertisement with credentials built from the reversed list of
 /// identities
-fn run_with_v0_creds<M, C>(b: &mut Bencher, identities: Vec<V0Identity<C>>, adv: &[u8])
-where
-    M: V0CryptoMaterialExt,
+fn run_with_v0_creds<C>(
+    b: &mut Bencher,
+    crypto_material_type: CryptoMaterialType,
+    identities: Vec<V0Identity<C>>,
+    adv: &[u8],
+) where
     C: CryptoProvider,
 {
     let mut creds = identities
         .into_iter()
-        .map(|i| {
-            let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&i.key_seed);
-            SimpleV0Credential::new(
-                M::build_cred::<C>(
-                    i.key_seed,
-                    hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&i.legacy_metadata_key),
-                ),
-                i.key_seed,
-            )
+        .map(|identity| identity.into_discovery_credential())
+        .map(|discovery_credential| MatchableCredential {
+            discovery_credential,
+            match_data: EmptyMatchedCredential,
         })
         .collect::<Vec<_>>();
 
@@ -269,34 +236,55 @@
     // cred source for predictably bad performance
     creds.reverse();
 
-    let cred_source = SliceCredentialSource::new(&creds);
-    b.iter(|| {
-        black_box(deserialize_v0_advertisement::<_, _, C>(adv, &cred_source).map(|_| 0_u8).unwrap())
-    });
+    match crypto_material_type {
+        CryptoMaterialType::MinFootprint => {
+            // Cache size of 0 => only min-footprint creds
+            let cred_book = CredentialBookBuilder::<_>::build_cached_slice_book::<
+                0,
+                0,
+                CryptoProviderImpl,
+            >(&creds, &[]);
+
+            b.iter(|| {
+                black_box(
+                    deserialize_advertisement::<_, C>(deserialization_arena!(), adv, &cred_book)
+                        .map(|_| 0_u8)
+                        .unwrap(),
+                )
+            });
+        }
+        CryptoMaterialType::Precalculated => {
+            let cred_book = CredentialBookBuilder::<_>::build_precalculated_owned_book::<C>(
+                creds,
+                core::iter::empty(),
+            );
+            b.iter(|| {
+                black_box(
+                    deserialize_advertisement::<_, C>(deserialization_arena!(), adv, &cred_book)
+                        .map(|_| 0_u8)
+                        .unwrap(),
+                )
+            });
+        }
+    }
 }
 
 /// Benchmark decrypting a V1 advertisement with credentials built from the reversed list of
 /// identities
-fn run_with_v1_creds<M, C>(b: &mut Bencher, identities: Vec<V1Identity<C>>, adv: &[u8])
-where
-    M: V1CryptoMaterialExt,
+fn run_with_v1_creds<C>(
+    b: &mut Bencher,
+    crypto_material_type: CryptoMaterialType,
+    identities: Vec<V1Identity<C>>,
+    adv: &[u8],
+) where
     C: CryptoProvider,
 {
     let mut creds = identities
         .into_iter()
-        .map(|i| {
-            let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&i.key_seed);
-            SimpleV1Credential::new(
-                M::build_cred::<C>(
-                    i.key_seed,
-                    hkdf.extended_unsigned_metadata_key_hmac_key()
-                        .calculate_hmac(&i.extended_metadata_key),
-                    hkdf.extended_signed_metadata_key_hmac_key()
-                        .calculate_hmac(&i.extended_metadata_key),
-                    i.key_pair.public(),
-                ),
-                i.key_seed,
-            )
+        .map(|identity| identity.into_discovery_credential())
+        .map(|discovery_credential| MatchableCredential {
+            discovery_credential,
+            match_data: EmptyMatchedCredential,
         })
         .collect::<Vec<_>>();
 
@@ -304,12 +292,39 @@
     // cred source for predictably bad performance
     creds.reverse();
 
-    let cred_source = SliceCredentialSource::new(&creds);
-    b.iter(|| {
-        black_box(deserialize_v1_advertisement::<_, _, C>(adv, &cred_source).map(|_| 0_u8).unwrap())
-    });
+    match crypto_material_type {
+        CryptoMaterialType::MinFootprint => {
+            // Cache size of 0 => only min-footprint creds
+            let cred_book = CredentialBookBuilder::<_>::build_cached_slice_book::<
+                0,
+                0,
+                CryptoProviderImpl,
+            >(&[], &creds);
+
+            b.iter(|| {
+                black_box(
+                    deserialize_advertisement::<_, C>(deserialization_arena!(), adv, &cred_book)
+                        .map(|_| 0_u8)
+                        .unwrap(),
+                )
+            });
+        }
+        CryptoMaterialType::Precalculated => {
+            let cred_book = CredentialBookBuilder::<_>::build_precalculated_owned_book::<C>(
+                core::iter::empty(),
+                creds,
+            );
+            b.iter(|| {
+                black_box(
+                    deserialize_advertisement::<_, C>(deserialization_arena!(), adv, &cred_book)
+                        .map(|_| 0_u8)
+                        .unwrap(),
+                )
+            });
+        }
+    }
 }
-fn add_des<I: SectionIdentity>(sb: &mut SectionBuilder<I>) {
+fn add_des<I: SectionEncoder>(sb: &mut SectionBuilder<I>) {
     sb.add_de_res(|_| TxPower::try_from(17).map(TxPowerDataElement::from)).unwrap();
     sb.add_de_res(|_| GenericDataElement::try_from(100_u32.into(), &[0; 10])).unwrap();
 }
@@ -324,20 +339,29 @@
 
 struct V0Identity<C: CryptoProvider> {
     key_seed: [u8; 32],
-    legacy_metadata_key: [u8; 14],
+    legacy_metadata_key: ShortMetadataKey,
     _marker: PhantomData<C>,
 }
 
 impl<C: CryptoProvider> V0Identity<C> {
     /// Generate a new identity with random crypto material
     fn random<R: rand::Rng + rand::CryptoRng>(rng: &mut R) -> Self {
-        Self { key_seed: rng.gen(), legacy_metadata_key: rng.gen(), _marker: PhantomData }
+        Self {
+            key_seed: rng.gen(),
+            legacy_metadata_key: ShortMetadataKey(rng.gen()),
+            _marker: PhantomData,
+        }
+    }
+    /// Convert this `V0Identity` into a V0 discovery credential.
+    fn into_discovery_credential(self) -> V0DiscoveryCredential {
+        SimpleBroadcastCryptoMaterial::<V0>::new(self.key_seed, self.legacy_metadata_key)
+            .derive_v0_discovery_credential::<C>()
     }
 }
 
 struct V1Identity<C: CryptoProvider> {
     key_seed: [u8; 32],
-    extended_metadata_key: [u8; 16],
+    extended_metadata_key: MetadataKey,
     key_pair: np_ed25519::KeyPair<C>,
 }
 
@@ -346,77 +370,23 @@
     fn random(rng: &mut C::CryptoRng) -> Self {
         Self {
             key_seed: rng.gen(),
-            extended_metadata_key: rng.gen(),
+            extended_metadata_key: MetadataKey(rng.gen()),
             key_pair: np_ed25519::KeyPair::<C>::generate(),
         }
     }
+    /// Convert this `V1Identity` into a `V1DiscoveryCredential`.
+    fn into_discovery_credential(self) -> V1DiscoveryCredential {
+        SimpleSignedBroadcastCryptoMaterial::new(
+            self.key_seed,
+            self.extended_metadata_key,
+            self.key_pair.private_key(),
+        )
+        .derive_v1_discovery_credential::<C>()
+    }
 }
 
-#[derive(strum_macros::EnumIter, Debug)]
+#[derive(strum_macros::EnumIter, Clone, Copy, Debug)]
 enum CryptoMaterialType {
     MinFootprint,
     Precalculated,
 }
-
-// if we get confident this is a valid shared way, could move this to the main trait
-trait V0CryptoMaterialExt: V0CryptoMaterial {
-    fn build_cred<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        legacy_metadata_key_hmac: [u8; 32],
-    ) -> Self;
-}
-
-trait V1CryptoMaterialExt: V1CryptoMaterial {
-    fn build_cred<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        unsigned_metadata_key_hmac: [u8; 32],
-        signed_metadata_key_hmac: [u8; 32],
-        pub_key: np_ed25519::PublicKey<C>,
-    ) -> Self;
-}
-
-impl V0CryptoMaterialExt for MinimumFootprintV0CryptoMaterial {
-    fn build_cred<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        legacy_metadata_key_hmac: [u8; 32],
-    ) -> Self {
-        Self::new(key_seed, legacy_metadata_key_hmac)
-    }
-}
-
-impl V0CryptoMaterialExt for PrecalculatedV0CryptoMaterial {
-    fn build_cred<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        legacy_metadata_key_hmac: [u8; 32],
-    ) -> Self {
-        Self::new::<C>(&key_seed, legacy_metadata_key_hmac)
-    }
-}
-
-impl V1CryptoMaterialExt for MinimumFootprintV1CryptoMaterial {
-    fn build_cred<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        unsigned_metadata_key_hmac: [u8; 32],
-        signed_metadata_key_hmac: [u8; 32],
-        pub_key: np_ed25519::PublicKey<C>,
-    ) -> Self {
-        Self::new(key_seed, unsigned_metadata_key_hmac, signed_metadata_key_hmac, pub_key)
-    }
-}
-
-impl V1CryptoMaterialExt for PrecalculatedV1CryptoMaterial {
-    fn build_cred<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        unsigned_metadata_key_hmac: [u8; 32],
-        signed_metadata_key_hmac: [u8; 32],
-        pub_key: np_ed25519::PublicKey<C>,
-    ) -> Self {
-        let min_foot = MinimumFootprintV1CryptoMaterial::new(
-            key_seed,
-            unsigned_metadata_key_hmac,
-            signed_metadata_key_hmac,
-            pub_key,
-        );
-        min_foot.to_precalculated::<C>()
-    }
-}
diff --git a/nearby/presence/np_adv/src/credential/book.rs b/nearby/presence/np_adv/src/credential/book.rs
new file mode 100644
index 0000000..9e6361d
--- /dev/null
+++ b/nearby/presence/np_adv/src/credential/book.rs
@@ -0,0 +1,542 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Traits defining credential books. These are used in the deserialization path to provide
+//! the credentials to try for either advertisement version.
+//! See [`CredentialBookBuilder`] for batteries-included implementations.
+
+use crate::credential::source::{
+    CredentialSource, DiscoveryCredentialSource, SliceCredentialSource,
+};
+use crate::credential::v0::{V0DiscoveryCryptoMaterial, V0};
+use crate::credential::v1::{
+    SignedSectionIdentityResolutionMaterial, SignedSectionVerificationMaterial,
+    UnsignedSectionIdentityResolutionMaterial, UnsignedSectionVerificationMaterial,
+    V1DiscoveryCryptoMaterial, V1,
+};
+#[cfg(feature = "alloc")]
+use crate::credential::ReferencedMatchedCredential;
+use crate::credential::{
+    DiscoveryCryptoMaterial, MatchableCredential, MatchedCredential, ProtocolVersion,
+};
+use core::borrow::Borrow;
+use core::marker::PhantomData;
+use crypto_provider::CryptoProvider;
+
+#[cfg(feature = "alloc")]
+use alloc::vec::Vec;
+
+/// A collection of credentials to try when attempting to deserialize
+/// advertisements of either advertisement version which is
+/// valid for the given lifetime.
+pub trait CredentialBook<'a>
+where
+    Self: 'a,
+{
+    /// The type of the matched credentials for the credentials
+    /// held within this credential-book. May lend from the credential-book.
+    type Matched: MatchedCredential;
+
+    /// The type of V0 crypto-materials yielded by this credential-book.
+    type V0Crypto: V0DiscoveryCryptoMaterial;
+
+    /// The type of V1 crypto-materials yielded by this credential-book.
+    type V1Crypto: V1DiscoveryCryptoMaterial;
+
+    /// The iterator type returned from [`CredentialBook#v0_iter()`],
+    /// which yields `(crypto-material, match_data)` pairs.
+    /// This is a lending iterator which may borrow things from `self`.
+    type V0Iterator: Iterator<Item = (Self::V0Crypto, Self::Matched)>;
+
+    /// The iterator type returned from [`CredentialBook#v1_iter()`],
+    /// which yields `(crypto-material, match_data)` pairs.
+    /// This is a lending iterator which may borrow things from `self`.
+    type V1Iterator: Iterator<Item = (Self::V1Crypto, Self::Matched)>;
+
+    /// Returns an iterator over V0 credentials in this credential book.
+    fn v0_iter(&'a self) -> Self::V0Iterator;
+
+    /// Returns an iterator over V1 credentials in this credential book.
+    fn v1_iter(&'a self) -> Self::V1Iterator;
+}
+
+/// Convenient marker struct for building credential-books with
+/// some given match-data type.
+pub struct CredentialBookBuilder<M: MatchedCredential> {
+    _marker: PhantomData<M>,
+}
+
+impl<M: MatchedCredential> CredentialBookBuilder<M> {
+    /// Constructs a [`CachedSliceCredentialBook`] from the given slices of discovery credentials,
+    /// populating its internal buffers of cached credential crypto-materials for up to the
+    /// given number of credentials in each version (up to `N0` credentials cached in V0,
+    /// and up to `N1` credentials cached in `V1`.)
+    pub fn build_cached_slice_book<'a, const N0: usize, const N1: usize, P: CryptoProvider>(
+        v0_creds: &'a [MatchableCredential<V0, M>],
+        v1_creds: &'a [MatchableCredential<V1, M>],
+    ) -> CachedSliceCredentialBook<'a, M, N0, N1> {
+        let v0_source = SliceCredentialSource::new(v0_creds);
+        let v0_cache = init_cache_from_source::<V0, _, N0, P>(&v0_source);
+        let v0_source = CachedCredentialSource::<_, _, N0>::new(v0_source, v0_cache);
+
+        let v1_source = SliceCredentialSource::new(v1_creds);
+        let v1_cache = init_cache_from_source::<V1, _, N1, P>(&v1_source);
+        let v1_source = CachedCredentialSource::<_, _, N1>::new(v1_source, v1_cache);
+
+        CredentialBookFromSources::new(v0_source, v1_source)
+    }
+
+    #[cfg(feature = "alloc")]
+    /// Constructs a new credential-book which owns all of the given credentials,
+    /// and maintains pre-calculated cryptographic information about them
+    /// for speedy advertisement deserialization.
+    pub fn build_precalculated_owned_book<P: CryptoProvider>(
+        v0_iter: impl IntoIterator<Item = MatchableCredential<V0, M>>,
+        v1_iter: impl IntoIterator<Item = MatchableCredential<V1, M>>,
+    ) -> PrecalculatedOwnedCredentialBook<M> {
+        let v0_source = PrecalculatedOwnedCredentialSource::<V0, M>::new::<P>(v0_iter);
+        let v1_source = PrecalculatedOwnedCredentialSource::<V1, M>::new::<P>(v1_iter);
+        CredentialBookFromSources::new(v0_source, v1_source)
+    }
+}
+
+// Now, for the implementation details. External implementors still need
+// to be able to reference these types (since the returned types from
+// [`CredentialBookBuilder`]'s convenience methods are just type-aliases),
+// and if they want, they can use them as building-blocks to construct
+// their own [`CredentialBook`]s, but they're largely just scaffolding.
+
+/// A structure bundling both V0 and V1 credential-sources to define
+/// a credential-book which owns both of these independent sources.
+pub struct CredentialBookFromSources<S0, S1> {
+    /// The source for V0 credentials.
+    v0_source: S0,
+    /// The source for V1 credentials.
+    v1_source: S1,
+}
+
+impl<'a, S0, S1> CredentialBookFromSources<S0, S1>
+where
+    Self: 'a,
+    S0: CredentialSource<'a, V0>,
+    S1: CredentialSource<'a, V1, Matched = <S0 as CredentialSource<'a, V0>>::Matched>,
+    <S0 as CredentialSource<'a, V0>>::Crypto: V0DiscoveryCryptoMaterial,
+    <S1 as CredentialSource<'a, V1>>::Crypto: V1DiscoveryCryptoMaterial,
+{
+    /// Constructs a new [`CredentialBook`] out of the two given
+    /// credential-sources, one for v0 and one for v1. The match-data
+    /// of both credential sources must be the same.
+    pub fn new(v0_source: S0, v1_source: S1) -> Self {
+        Self { v0_source, v1_source }
+    }
+}
+
+impl<'a, S0, S1> CredentialBook<'a> for CredentialBookFromSources<S0, S1>
+where
+    Self: 'a,
+    S0: CredentialSource<'a, V0>,
+    S1: CredentialSource<'a, V1, Matched = <S0 as CredentialSource<'a, V0>>::Matched>,
+    <S0 as CredentialSource<'a, V0>>::Crypto: V0DiscoveryCryptoMaterial,
+    <S1 as CredentialSource<'a, V1>>::Crypto: V1DiscoveryCryptoMaterial,
+{
+    type Matched = <S0 as CredentialSource<'a, V0>>::Matched;
+    type V0Crypto = <S0 as CredentialSource<'a, V0>>::Crypto;
+    type V1Crypto = <S1 as CredentialSource<'a, V1>>::Crypto;
+    type V0Iterator = <S0 as CredentialSource<'a, V0>>::Iterator;
+    type V1Iterator = <S1 as CredentialSource<'a, V1>>::Iterator;
+
+    fn v0_iter(&'a self) -> Self::V0Iterator {
+        self.v0_source.iter()
+    }
+    fn v1_iter(&'a self) -> Self::V1Iterator {
+        self.v1_source.iter()
+    }
+}
+
+/// Type-level function used internally
+/// by [`CachedCredentialSource`] in order to uniformly
+/// refer to the "precalculated" crypto-material variants
+/// for each protocol version.
+pub(crate) mod precalculated_for_version {
+    use crate::credential::v0::{
+        PrecalculatedV0DiscoveryCryptoMaterial, V0DiscoveryCredential, V0,
+    };
+    use crate::credential::v1::{
+        PrecalculatedV1DiscoveryCryptoMaterial, V1DiscoveryCredential, V1,
+    };
+    use crate::credential::{DiscoveryCryptoMaterial, ProtocolVersion};
+    use crypto_provider::CryptoProvider;
+
+    pub trait MappingTrait<V: ProtocolVersion> {
+        type Output: DiscoveryCryptoMaterial<V>;
+        /// Provides pre-calculated crypto-material for the given
+        /// discovery credential.
+        fn precalculate<C: CryptoProvider>(
+            discovery_credential: &V::DiscoveryCredential,
+        ) -> Self::Output;
+    }
+    pub enum Marker {}
+    impl MappingTrait<V0> for Marker {
+        type Output = PrecalculatedV0DiscoveryCryptoMaterial;
+        fn precalculate<C: CryptoProvider>(
+            discovery_credential: &V0DiscoveryCredential,
+        ) -> PrecalculatedV0DiscoveryCryptoMaterial {
+            PrecalculatedV0DiscoveryCryptoMaterial::new::<C>(discovery_credential)
+        }
+    }
+    impl MappingTrait<V1> for Marker {
+        type Output = PrecalculatedV1DiscoveryCryptoMaterial;
+        fn precalculate<C: CryptoProvider>(
+            discovery_credential: &V1DiscoveryCredential,
+        ) -> PrecalculatedV1DiscoveryCryptoMaterial {
+            discovery_credential.to_precalculated::<C>()
+        }
+    }
+}
+
+type PrecalculatedCryptoForProtocolVersion<V> =
+    <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<V>>::Output;
+
+fn precalculate_crypto_material<V: ProtocolVersion, C: CryptoProvider>(
+    discovery_credential: &V::DiscoveryCredential,
+) -> PrecalculatedCryptoForProtocolVersion<V>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    <precalculated_for_version::Marker as precalculated_for_version::MappingTrait<V>>::precalculate::<
+        C,
+    >(discovery_credential)
+}
+
+/// Iterator type for [`CachedCredentialSource`].
+pub struct CachedCredentialSourceIterator<
+    'a,
+    V: ProtocolVersion + 'a,
+    S: DiscoveryCredentialSource<'a, V> + 'a,
+    const N: usize,
+> where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    /// The current index of the (enumerated) elements that we're iterating over.
+    /// Always counts up at every iteration, and may lie outside of the range
+    /// of the `cache` slice for uncached elements.
+    current_index: usize,
+    /// The cache of pre-calculated crypto-materials from the original source.
+    ///
+    /// The [`self.source_iterator`]'s (up-to) first `N` elements
+    /// must match (up-to) the first `N` elements in the cache,
+    /// and they must both appear in the same order.
+    cache: &'a [Option<PrecalculatedCryptoForProtocolVersion<V>>; N],
+    /// The iterator over the credentials in the original source
+    source_iterator: S::Iterator,
+}
+
+impl<'a, V: ProtocolVersion + 'a, S: DiscoveryCredentialSource<'a, V> + 'a, const N: usize> Iterator
+    for CachedCredentialSourceIterator<'a, V, S, N>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    type Item = (PossiblyCachedDiscoveryCryptoMaterial<'a, V>, S::Matched);
+    fn next(&mut self) -> Option<Self::Item> {
+        // Regardless of what we're going to do with the crypto-materials, we still need
+        // to advance the underlying iterator, and bail if it runs out.
+        let (discovery_credential, match_data) = self.source_iterator.next()?;
+        // Check whether/not we have cached crypto-material for the current index
+        let crypto = match self.cache.get(self.current_index) {
+            Some(precalculated_crypto_entry) => {
+                let precalculated_crypto = precalculated_crypto_entry
+                    .as_ref()
+                    .expect("iterator still going, but cache is not full?");
+                PossiblyCachedDiscoveryCryptoMaterial::from_precalculated(precalculated_crypto)
+            }
+            None => PossiblyCachedDiscoveryCryptoMaterial::from_discovery_credential(
+                discovery_credential.borrow().clone(),
+            ),
+        };
+        // Advance the index forward to point to the next item in the cache.
+        self.current_index += 1;
+        Some((crypto, match_data))
+    }
+}
+
+/// A [`CredentialSource`] which augments the externally-provided [`DiscoveryCredentialSource`] with
+/// a cache containing up to the specified number of pre-calculated credentials.
+pub struct CachedCredentialSource<V: ProtocolVersion, S, const N: usize>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    wrapped: S,
+    cache: [Option<PrecalculatedCryptoForProtocolVersion<V>>; N],
+}
+
+/// Helper function to construct a cache for a [`CachedCredentialSource`] from
+/// a reference to some source of discovery-credentials.
+///
+/// This function needs to be kept separate from [`CachedCredentialSource#new`]
+/// to get around otherwise-tricky lifetime issues around ephemerally-borrowing
+/// from a moved object within the body of a function.
+pub(crate) fn init_cache_from_source<'a, V: ProtocolVersion, S, const N: usize, P: CryptoProvider>(
+    original: &'a S,
+) -> [Option<PrecalculatedCryptoForProtocolVersion<V>>; N]
+where
+    S: DiscoveryCredentialSource<'a, V> + 'a,
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    let mut cache = [0u8; N].map(|_| None);
+    for (cache_entry, source_credential) in cache.iter_mut().zip(original.iter()) {
+        let (discovery_credential, _) = source_credential;
+        let precalculated_crypto_material =
+            precalculate_crypto_material::<V, P>(discovery_credential.borrow());
+        let _ = cache_entry.insert(precalculated_crypto_material);
+    }
+    cache
+}
+
+impl<'a, V: ProtocolVersion, S, const N: usize> CachedCredentialSource<V, S, N>
+where
+    S: DiscoveryCredentialSource<'a, V> + 'a,
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    /// Constructs a [`CachedCredentialSource`] from the given [`DiscoveryCredentialSource`]
+    /// and the given (initial) cache contents, as constructed via the
+    /// [`init_cache_from_source`] helper function.
+    pub(crate) fn new(
+        wrapped: S,
+        cache: [Option<PrecalculatedCryptoForProtocolVersion<V>>; N],
+    ) -> Self {
+        Self { wrapped, cache }
+    }
+}
+
+/// Internal implementation of [`PossiblyCachedDiscoveryCryptoMaterial`] to hide
+/// what crypto-material variants we're actually storing in cached
+/// credential-books.
+pub(crate) enum PossiblyCachedDiscoveryCryptoMaterialKind<'a, V: ProtocolVersion>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    Discovery(V::DiscoveryCredential),
+    Precalculated(&'a PrecalculatedCryptoForProtocolVersion<V>),
+}
+
+/// Crypto-materials that are potentially references to
+/// already-cached precomputed variants, or raw discovery
+/// credentials.
+pub struct PossiblyCachedDiscoveryCryptoMaterial<'a, V: ProtocolVersion>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    pub(crate) wrapped: PossiblyCachedDiscoveryCryptoMaterialKind<'a, V>,
+}
+
+impl<'a, V: ProtocolVersion> PossiblyCachedDiscoveryCryptoMaterial<'a, V>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    fn from_discovery_credential(discovery_credential: V::DiscoveryCredential) -> Self {
+        Self { wrapped: PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(discovery_credential) }
+    }
+    fn from_precalculated(precalculated: &'a PrecalculatedCryptoForProtocolVersion<V>) -> Self {
+        Self { wrapped: PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(precalculated) }
+    }
+}
+
+impl<'a, V: ProtocolVersion> DiscoveryCryptoMaterial<V>
+    for PossiblyCachedDiscoveryCryptoMaterial<'a, V>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        match &self.wrapped {
+            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => x.metadata_nonce::<C>(),
+            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => x.metadata_nonce::<C>(),
+        }
+    }
+}
+
+impl<'a> V0DiscoveryCryptoMaterial for PossiblyCachedDiscoveryCryptoMaterial<'a, V0> {
+    fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C> {
+        match &self.wrapped {
+            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => x.ldt_adv_cipher::<C>(),
+            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => x.ldt_adv_cipher::<C>(),
+        }
+    }
+}
+
+impl<'a> V1DiscoveryCryptoMaterial for PossiblyCachedDiscoveryCryptoMaterial<'a, V1> {
+    fn signed_identity_resolution_material<C: CryptoProvider>(
+        &self,
+    ) -> SignedSectionIdentityResolutionMaterial {
+        match &self.wrapped {
+            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
+                x.signed_identity_resolution_material::<C>()
+            }
+            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
+                x.signed_identity_resolution_material::<C>()
+            }
+        }
+    }
+
+    fn unsigned_identity_resolution_material<C: CryptoProvider>(
+        &self,
+    ) -> UnsignedSectionIdentityResolutionMaterial {
+        match &self.wrapped {
+            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
+                x.unsigned_identity_resolution_material::<C>()
+            }
+            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
+                x.unsigned_identity_resolution_material::<C>()
+            }
+        }
+    }
+
+    fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
+        match &self.wrapped {
+            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
+                x.signed_verification_material::<C>()
+            }
+            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
+                x.signed_verification_material::<C>()
+            }
+        }
+    }
+
+    fn unsigned_verification_material<C: CryptoProvider>(
+        &self,
+    ) -> UnsignedSectionVerificationMaterial {
+        match &self.wrapped {
+            PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(x) => {
+                x.unsigned_verification_material::<C>()
+            }
+            PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(x) => {
+                x.unsigned_verification_material::<C>()
+            }
+        }
+    }
+}
+
+impl<'a, V: ProtocolVersion, S, const N: usize> CredentialSource<'a, V>
+    for CachedCredentialSource<V, S, N>
+where
+    Self: 'a,
+    S: DiscoveryCredentialSource<'a, V> + 'a,
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+    PossiblyCachedDiscoveryCryptoMaterial<'a, V>: DiscoveryCryptoMaterial<V>,
+{
+    type Matched = <S as DiscoveryCredentialSource<'a, V>>::Matched;
+    type Crypto = PossiblyCachedDiscoveryCryptoMaterial<'a, V>;
+    type Iterator = CachedCredentialSourceIterator<'a, V, S, N>;
+
+    fn iter(&'a self) -> Self::Iterator {
+        CachedCredentialSourceIterator {
+            current_index: 0,
+            cache: &self.cache,
+            source_iterator: self.wrapped.iter(),
+        }
+    }
+}
+
+/// A simple credentials source for environments which are,
+/// for all practical purposes, not space-constrained, and hence
+/// can store an arbitrary amount of pre-calculated crypto-materials.
+///
+/// Requires `alloc` as a result of internally leveraging a `Vec`.
+#[cfg(feature = "alloc")]
+pub struct PrecalculatedOwnedCredentialSource<V: ProtocolVersion, M: MatchedCredential>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    credentials: Vec<(PrecalculatedCryptoForProtocolVersion<V>, M)>,
+}
+
+#[cfg(feature = "alloc")]
+impl<'a, V: ProtocolVersion + 'a, M: MatchedCredential + 'a>
+    PrecalculatedOwnedCredentialSource<V, M>
+where
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+{
+    /// Pre-calculates crypto material for the given credentials, and constructs a
+    /// credentials source which holds owned copies of this crypto-material.
+    pub fn new<P: CryptoProvider>(
+        credential_iter: impl IntoIterator<Item = MatchableCredential<V, M>>,
+    ) -> Self {
+        let credentials = credential_iter
+            .into_iter()
+            .map(|credential| {
+                (
+                    precalculate_crypto_material::<V, P>(&credential.discovery_credential),
+                    credential.match_data,
+                )
+            })
+            .collect();
+        Self { credentials }
+    }
+}
+
+#[cfg(feature = "alloc")]
+fn reference_crypto_and_match_data<C, M: MatchedCredential>(
+    pair_ref: &(C, M),
+) -> (&C, ReferencedMatchedCredential<M>) {
+    let (c, m) = pair_ref;
+    (c, m.into())
+}
+
+#[cfg(feature = "alloc")]
+impl<'a, V: ProtocolVersion, M: MatchedCredential> CredentialSource<'a, V>
+    for PrecalculatedOwnedCredentialSource<V, M>
+where
+    Self: 'a,
+    precalculated_for_version::Marker: precalculated_for_version::MappingTrait<V>,
+    &'a PrecalculatedCryptoForProtocolVersion<V>: DiscoveryCryptoMaterial<V>,
+{
+    type Matched = ReferencedMatchedCredential<'a, M>;
+    type Crypto = &'a PrecalculatedCryptoForProtocolVersion<V>;
+    type Iterator = core::iter::Map<
+        core::slice::Iter<'a, (PrecalculatedCryptoForProtocolVersion<V>, M)>,
+        fn(
+            &'a (PrecalculatedCryptoForProtocolVersion<V>, M),
+        )
+            -> (&'a PrecalculatedCryptoForProtocolVersion<V>, ReferencedMatchedCredential<'a, M>),
+    >;
+
+    fn iter(&'a self) -> Self::Iterator {
+        self.credentials.iter().map(reference_crypto_and_match_data)
+    }
+}
+
+/// Type-alias for credential sources which are provided via slice credential-sources
+/// with a pre-calculated credential cache layered on top.
+pub type CachedSliceCredentialSource<'a, V, M, const N: usize> =
+    CachedCredentialSource<V, SliceCredentialSource<'a, V, M>, N>;
+
+/// A [`CredentialBook`] whose sources for V0 and V1 credentials come from the given slices
+/// of discovery credentials, with crypto-materials for up to the given number of credentials
+/// from the beginning of each slice kept in an in-memory cache.
+pub type CachedSliceCredentialBook<'a, M, const N0: usize, const N1: usize> =
+    CredentialBookFromSources<
+        CachedSliceCredentialSource<'a, V0, M, N0>,
+        CachedSliceCredentialSource<'a, V1, M, N1>,
+    >;
+
+#[cfg(feature = "alloc")]
+/// A credential-book which owns all of its (non-matched) credential data,
+/// and maintains pre-calculated cryptographic information about all
+/// stored credentials for speedy advertisement deserialization.
+///
+/// Use this credential book if memory usage is not terribly tight,
+/// and you're operating in an environment with an allocator.
+pub type PrecalculatedOwnedCredentialBook<M> = CredentialBookFromSources<
+    PrecalculatedOwnedCredentialSource<V0, M>,
+    PrecalculatedOwnedCredentialSource<V1, M>,
+>;
diff --git a/nearby/presence/np_adv/src/credential/mod.rs b/nearby/presence/np_adv/src/credential/mod.rs
index a138f0b..3cab51e 100644
--- a/nearby/presence/np_adv/src/credential/mod.rs
+++ b/nearby/presence/np_adv/src/credential/mod.rs
@@ -18,68 +18,375 @@
 //! efficiency gains with implementations tailored to suit (e.g. caching a few hot credentials
 //! rather than reading from disk every time, etc).
 
+use crate::MetadataKey;
+
+use crate::credential::v0::{V0DiscoveryCredential, V0ProtocolVersion};
+
+use core::convert::Infallible;
 use core::fmt::Debug;
-use crypto_provider::CryptoProvider;
+use crypto_provider::{CryptoProvider, CryptoRng};
 
-use self::{
-    simple::{SimpleV0Credential, SimpleV1Credential},
-    source::OwnedBothCredentialSource,
-    v0::MinimumFootprintV0CryptoMaterial,
-    v1::MinimumFootprintV1CryptoMaterial,
-};
-
-pub mod simple;
+pub mod book;
 pub mod source;
+#[cfg(test)]
+pub mod tests;
 pub mod v0;
 pub mod v1;
 
-/// A credential which has an associated [`MatchedCredential`]
-pub trait MatchableCredential {
-    /// The [MatchedCredential] provided by this [`MatchableCredential`].
-    type Matched<'m>: MatchedCredential<'m>
-    where
-        Self: 'm;
-
-    /// Returns the subset of credential data that should be associated with a successfully
-    /// decrypted advertisement or advertisement section.
-    fn matched(&self) -> Self::Matched<'_>;
+/// Information about a credential as supplied by the caller.
+#[derive(Clone)]
+pub struct MatchableCredential<V: ProtocolVersion, M: MatchedCredential> {
+    /// The discovery credential/cryptographic information associated
+    /// with this particular credential which is used for discovering
+    /// advertisements/advertisement sections generated via the
+    /// paired sender credential.
+    pub discovery_credential: V::DiscoveryCredential,
+    /// The data which will be yielded back to the caller upon a successful
+    /// identity-match against this credential.
+    pub match_data: M,
 }
 
-/// Convenient type-level function for referring to the match data for a [`MatchableCredential`].
-pub type MatchedCredFromCred<'s, C> = <C as MatchableCredential>::Matched<'s>;
+impl<V: ProtocolVersion, M: MatchedCredential> MatchableCredential<V, M> {
+    /// De-structures this credential into the pairing of a discovery
+    /// credential and some matched credential data.
+    pub fn into_pair(self) -> (V::DiscoveryCredential, M) {
+        (self.discovery_credential, self.match_data)
+    }
+    /// Views this credential as a (borrowed) discovery-credential
+    /// combined with some matched credential data
+    /// (which is copied - see documentation on [`MatchedCredential`])
+    pub fn as_pair(&self) -> (&V::DiscoveryCredential, ReferencedMatchedCredential<M>) {
+        (&self.discovery_credential, ReferencedMatchedCredential::from(&self.match_data))
+    }
+}
 
 /// The portion of a credential's data to be bundled with the advertisement content it was used to
-/// decrypt.
+/// decrypt. At a minimum, this includes any encrypted identity-specific metadata.
 ///
-/// As it is `Debug` and `Eq`, implementors should not hold any cryptographic material to avoid
+/// As it is `Debug` and `Eq`, implementors should not hold any cryptographic secrets to avoid
 /// accidental logging, timing side channels on comparison, etc, or should use custom impls of
 /// those traits rather than deriving them.
-pub trait MatchedCredential<'m>: Debug + PartialEq + Eq {}
+///
+/// Instances of `MatchedCredential` may be cloned whenever advertisement content is
+/// successfully associated with a credential (see [`crate::WithMatchedCredential`]). As a
+/// result, it's recommended to use matched-credentials which reference
+/// some underlying match-data, but don't necessarily own it.
+/// See [`ReferencedMatchedCredential`] for the most common case of shared references.
+pub trait MatchedCredential: Debug + PartialEq + Eq + Clone {
+    /// The type returned for successful calls to [`Self::fetch_encrypted_metadata`].
+    type EncryptedMetadata: AsRef<[u8]>;
 
-/// A V0 credential containing some [`v0::V0CryptoMaterial`]
-pub trait V0Credential: MatchableCredential {
-    /// The [v0::V0CryptoMaterial] provided by this V0Credential impl.
-    type CryptoMaterial: v0::V0CryptoMaterial;
+    /// The type of errors for [`Self::fetch_encrypted_metadata`].
+    type EncryptedMetadataFetchError: Debug;
 
-    /// Returns the crypto material associated with the credential.
+    /// Attempts to obtain the (AES-GCM)-encrypted metadata bytes for the credential,
+    /// with possible failure based on the availability of the underlying data (i.e:
+    /// failing disk reads.)
     ///
-    /// Used to decrypted encrypted advertisement content.
-    fn crypto_material(&self) -> &Self::CryptoMaterial;
+    /// If your implementation does not maintain any encrypted metadata for each credential,
+    /// you may simply return an empty byte-array from this method.
+    ///
+    /// If your method for obtaining metadata cannot fail, use
+    /// the `core::convert::Infallible` type for the error type
+    /// [`Self::EncryptedMetadataFetchError`].
+    fn fetch_encrypted_metadata(
+        &self,
+    ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError>;
 }
 
-/// A V1 credential containing some [`v1::V1CryptoMaterial`]
-pub trait V1Credential: MatchableCredential {
-    /// The [v1::V1CryptoMaterial] provided by this Credential impl.
-    type CryptoMaterial: v1::V1CryptoMaterial;
-
-    /// Returns the crypto material associated with the credential.
-    ///
-    /// Used to decrypt encrypted advertisement content.
-    fn crypto_material(&self) -> &Self::CryptoMaterial;
+/// [`MatchedCredential`] wrapper around a shared reference to a [`MatchedCredential`].
+/// This is done instead of providing a blanket impl of [`MatchedCredential`] for
+/// reference types to allow for downstream crates to impl [`MatchedCredential`] on
+/// specific reference types.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct ReferencedMatchedCredential<'a, M: MatchedCredential> {
+    wrapped: &'a M,
 }
 
-/// An owned credential store that contains minimum footprint crypto materials
-pub type MinimumFootprintCredentialSource<T> = OwnedBothCredentialSource<
-    SimpleV0Credential<MinimumFootprintV0CryptoMaterial, T>,
-    SimpleV1Credential<MinimumFootprintV1CryptoMaterial, T>,
->;
+impl<'a, M: MatchedCredential> From<&'a M> for ReferencedMatchedCredential<'a, M> {
+    fn from(wrapped: &'a M) -> Self {
+        Self { wrapped }
+    }
+}
+
+impl<'a, M: MatchedCredential> AsRef<M> for ReferencedMatchedCredential<'a, M> {
+    fn as_ref(&self) -> &M {
+        self.wrapped
+    }
+}
+
+impl<'a, M: MatchedCredential> MatchedCredential for ReferencedMatchedCredential<'a, M> {
+    type EncryptedMetadata = <M as MatchedCredential>::EncryptedMetadata;
+    type EncryptedMetadataFetchError = <M as MatchedCredential>::EncryptedMetadataFetchError;
+    fn fetch_encrypted_metadata(
+        &self,
+    ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> {
+        self.wrapped.fetch_encrypted_metadata()
+    }
+}
+
+/// A simple implementation of [`MatchedCredential`] where all match-data
+/// is contained in the encrypted metadata byte-field.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct MetadataMatchedCredential<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> {
+    encrypted_metadata: A,
+}
+
+#[cfg(any(test, feature = "alloc"))]
+impl MetadataMatchedCredential<alloc::vec::Vec<u8>> {
+    /// Builds a [`MetadataMatchedCredential`] whose contents are given
+    /// as plaintext to be encrypted using AES-GCM against the given
+    /// broadcast crypto-material.
+    pub fn encrypt_from_plaintext<V, B, C>(broadcast_cm: &B, plaintext_metadata: &[u8]) -> Self
+    where
+        V: ProtocolVersion,
+        B: BroadcastCryptoMaterial<V>,
+        C: CryptoProvider,
+    {
+        let encrypted_metadata = broadcast_cm.encrypt_metadata::<C>(plaintext_metadata);
+        Self { encrypted_metadata }
+    }
+}
+
+impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MetadataMatchedCredential<A> {
+    /// Builds a new [`MetadataMatchedCredential`] with the given
+    /// encrypted metadata.
+    pub fn new(encrypted_metadata: A) -> Self {
+        Self { encrypted_metadata }
+    }
+}
+
+impl<A: AsRef<[u8]> + Clone + Debug + PartialEq + Eq> MatchedCredential
+    for MetadataMatchedCredential<A>
+{
+    type EncryptedMetadata = A;
+    type EncryptedMetadataFetchError = Infallible;
+    fn fetch_encrypted_metadata(&self) -> Result<Self::EncryptedMetadata, Infallible> {
+        Ok(self.encrypted_metadata.clone())
+    }
+}
+
+/// Trivial implementation of [`MatchedCredential`] which consists of no match-data.
+/// Suitable for usage scenarios where the decoded advertisement contents matter,
+/// but not necessarily which devices generated the contents.
+///
+/// Attempting to obtain the encrypted metadata from this type of credential
+/// will always yield an empty byte-array.
+#[derive(Default, Debug, PartialEq, Eq, Clone)]
+pub struct EmptyMatchedCredential;
+
+impl MatchedCredential for EmptyMatchedCredential {
+    type EncryptedMetadata = [u8; 0];
+    type EncryptedMetadataFetchError = Infallible;
+    fn fetch_encrypted_metadata(
+        &self,
+    ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> {
+        Ok([0u8; 0])
+    }
+}
+
+#[cfg(any(test, feature = "devtools"))]
+/// A [`MatchedCredential`] which consists only of the `key_seed` in the crypto-material
+/// for the credential. Note that this is unique per-credential by construction,
+/// and so this provides natural match-data for credentials in settings where
+/// there may not be any other information available.
+///
+/// Since this matched-credential type contains cryptographic information mirroring
+/// a credential's crypto-material, this structure is not suitable for production
+/// usage outside of unit tests and dev-tools.
+///
+/// Additionally, note that the metadata on this particular kind of matched credential
+/// is deliberately made inaccessible. This is done because a key-seed representation
+/// is only suitable in very limited circumstances where no other meaningful
+/// identifying information is available, such as that which is contained in metadata.
+/// Attempting to obtain the encrypted metadata from this type of matched credential
+/// will always yield an empty byte-array.
+#[derive(Default, Debug, PartialEq, Eq, Clone)]
+pub struct KeySeedMatchedCredential {
+    key_seed: [u8; 32],
+}
+
+#[cfg(any(test, feature = "devtools"))]
+impl From<[u8; 32]> for KeySeedMatchedCredential {
+    fn from(key_seed: [u8; 32]) -> Self {
+        Self { key_seed }
+    }
+}
+#[cfg(any(test, feature = "devtools"))]
+impl From<KeySeedMatchedCredential> for [u8; 32] {
+    fn from(matched: KeySeedMatchedCredential) -> Self {
+        matched.key_seed
+    }
+}
+
+#[cfg(any(test, feature = "devtools"))]
+impl MatchedCredential for KeySeedMatchedCredential {
+    type EncryptedMetadata = [u8; 0];
+    type EncryptedMetadataFetchError = Infallible;
+    fn fetch_encrypted_metadata(
+        &self,
+    ) -> Result<Self::EncryptedMetadata, Self::EncryptedMetadataFetchError> {
+        Ok([0u8; 0])
+    }
+}
+
+/// Error returned when metadata decryption fails.
+#[derive(Debug, Eq, PartialEq)]
+pub struct MetadataDecryptionError;
+
+/// Seal for protocol versions to enforce totality.
+pub(crate) mod protocol_version_seal {
+    /// Internal-only supertrait of protocol versions
+    /// for the purpose of sealing the trait.
+    pub trait ProtocolVersionSeal: Clone {}
+}
+
+/// Marker trait for protocol versions (V0/V1)
+/// and associated data about them.
+pub trait ProtocolVersion: protocol_version_seal::ProtocolVersionSeal {
+    /// The discovery credential type for this protocol version, which
+    /// is the minimal amount of cryptographic materials that we need
+    /// in order to discover advertisements/sections which make
+    /// use of the sender-paired version of the credential.
+    type DiscoveryCredential: DiscoveryCryptoMaterial<Self> + Clone;
+
+    /// The native-length metadata key for this protocol version
+    /// [i.e: if V0, a 14-byte metadata key, or if V1, a 16-byte
+    /// metadata key.]
+    type MetadataKey: Clone + AsRef<[u8]>;
+
+    /// Computes the metadata nonce for this version from the given key-seed.
+    fn metadata_nonce_from_key_seed<C: CryptoProvider>(key_seed: &[u8; 32]) -> [u8; 12];
+
+    /// Expands the passed metadata key (if needed) to a 16-byte metadata key
+    /// which may be used for metadata encryption/decryption
+    fn expand_metadata_key<C: CryptoProvider>(metadata_key: Self::MetadataKey) -> MetadataKey;
+
+    /// Generates a random metadata key using the given cryptographically-secure Rng
+    fn gen_random_metadata_key<R: CryptoRng>(rng: &mut R) -> Self::MetadataKey;
+
+    #[cfg(any(test, feature = "alloc"))]
+    /// Decrypt the given metadata using the given metadata nonce and version-specific
+    /// metadata key. Returns [`MetadataDecryptionError`] in the case that
+    /// the decryption operation failed.
+    fn decrypt_metadata<C: CryptoProvider>(
+        metadata_nonce: [u8; 12],
+        metadata_key: Self::MetadataKey,
+        encrypted_metadata: &[u8],
+    ) -> Result<alloc::vec::Vec<u8>, MetadataDecryptionError> {
+        use crypto_provider::{
+            aead::{Aead, AeadInit},
+            aes::Aes128Key,
+        };
+
+        let metadata_key = Self::expand_metadata_key::<C>(metadata_key);
+        let metadata_key = Aes128Key::from(metadata_key.0);
+        let aead = <<C as CryptoProvider>::Aes128Gcm as AeadInit<Aes128Key>>::new(&metadata_key);
+        // No additional authenticated data for encrypted metadata.
+        aead.decrypt(encrypted_metadata, &[], &metadata_nonce).map_err(|_| MetadataDecryptionError)
+    }
+}
+
+/// Trait for structures which provide cryptographic
+/// materials for discovery in a particular protocol version.
+/// See [`crate::credential::v0::V0DiscoveryCryptoMaterial`]
+/// and [`crate::credential::v1::V1DiscoveryCryptoMaterial`]
+/// for V0 and V1 specializations.
+pub trait DiscoveryCryptoMaterial<V: ProtocolVersion> {
+    /// Constructs or copies the metadata nonce used for decryption of associated credential
+    /// metadata for the identity represented via this crypto material.
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12];
+}
+
+/// Cryptographic materials necessary for broadcasting encrypted
+/// advertisement contents with the given protocol version.
+pub trait BroadcastCryptoMaterial<V: ProtocolVersion> {
+    /// Yields a copy of the key seed to be used to derive other key materials used
+    /// in the encryption of broadcasted advertisement contents.
+    fn key_seed(&self) -> [u8; 32];
+
+    /// Yields a copy of the metadata-key (size dependent on protocol version)
+    /// to tag advertisement contents sent with this broadcast crypto-material.
+    fn metadata_key(&self) -> V::MetadataKey;
+
+    /// Yields the 16-byte expanded metadata key, suitable for metadata encryption.
+    fn expanded_metadata_key<C: CryptoProvider>(&self) -> MetadataKey {
+        V::expand_metadata_key::<C>(self.metadata_key())
+    }
+
+    /// Constructs the metadata nonce used for encryption of associated credential
+    /// metadata for the identity represented via this crypto material.
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        V::metadata_nonce_from_key_seed::<C>(&self.key_seed())
+    }
+
+    /// Derives a V0 discovery credential from this V0 broadcast crypto-material
+    /// which may be used to discover v0 advertisements broadcasted with this credential.`
+    fn derive_v0_discovery_credential<C: CryptoProvider>(&self) -> V0DiscoveryCredential
+    where
+        V: V0ProtocolVersion,
+    {
+        let key_seed = self.key_seed();
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&key_seed);
+        let metadata_key_hmac =
+            hkdf.legacy_metadata_key_hmac_key().calculate_hmac(self.metadata_key().as_ref());
+        V0DiscoveryCredential::new(key_seed, metadata_key_hmac)
+    }
+
+    #[cfg(any(test, feature = "alloc"))]
+    /// Encrypts the given plaintext metadata bytes to allow that metadata
+    /// to be shared with receiving devices.
+    fn encrypt_metadata<C: CryptoProvider>(
+        &self,
+        plaintext_metadata: &[u8],
+    ) -> alloc::vec::Vec<u8> {
+        use crypto_provider::{
+            aead::{Aead, AeadInit},
+            aes::Aes128Key,
+        };
+        let plaintext_metadata_key = self.expanded_metadata_key::<C>();
+        let plaintext_metadata_key = Aes128Key::from(plaintext_metadata_key.0);
+
+        let aead =
+            <<C as CryptoProvider>::Aes128Gcm as AeadInit<Aes128Key>>::new(&plaintext_metadata_key);
+        // No additional authenticated data for encrypted metadata.
+        aead.encrypt(plaintext_metadata, &[], &self.metadata_nonce::<C>())
+            .expect("Metadata encryption should be infallible")
+    }
+}
+
+/// Concrete implementation of [`BroadcastCryptoMaterial<V>`] for
+/// a particular protocol version which keeps the key seed
+/// and the metadata key contiguous in memory.
+///
+/// Broadcast crypto-material specified in this way will only
+/// be usable for (unsigned) advertisement content broadcasts
+/// in the given protocol version.
+///
+/// For more flexible expression of broadcast credentials,
+/// feel free to directly implement one or more of the
+/// [`BroadcastCryptoMaterial`] and/or
+/// [`crate::credential::v1::SignedBroadcastCryptoMaterial`]
+/// traits on your own struct, dependent on the details
+/// of your own broadcast credentials.
+pub struct SimpleBroadcastCryptoMaterial<V: ProtocolVersion> {
+    key_seed: [u8; 32],
+    metadata_key: V::MetadataKey,
+}
+
+impl<V: ProtocolVersion> SimpleBroadcastCryptoMaterial<V> {
+    /// Builds some simple broadcast crypto-materials out of
+    /// the provided key-seed and version-specific metadata-key.
+    pub fn new(key_seed: [u8; 32], metadata_key: V::MetadataKey) -> Self {
+        Self { key_seed, metadata_key }
+    }
+}
+
+impl<V: ProtocolVersion> BroadcastCryptoMaterial<V> for SimpleBroadcastCryptoMaterial<V> {
+    fn key_seed(&self) -> [u8; 32] {
+        self.key_seed
+    }
+    fn metadata_key(&self) -> V::MetadataKey {
+        self.metadata_key.clone()
+    }
+}
diff --git a/nearby/presence/np_adv/src/credential/simple.rs b/nearby/presence/np_adv/src/credential/simple.rs
deleted file mode 100644
index 6ea2436..0000000
--- a/nearby/presence/np_adv/src/credential/simple.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Simple implementations of credentials. These can be combined with the provided crypto material
-//! implementations to have a working credential type.
-//!
-//! ```rust
-//! use np_adv::credential::{
-//!     simple::SimpleV0Credential,
-//!     v0::MinimumFootprintV0CryptoMaterial,
-//! };
-//! type MyV0Credential = SimpleV0Credential<MinimumFootprintV0CryptoMaterial, ()>;
-//! ```
-
-use core::fmt::Debug;
-
-use super::*;
-use super::{v0::*, v1::*};
-
-/// A simple implementation of [`V0Credential`] that wraps a [`V0CryptoMaterial`] and some `T` data that
-/// will be exposed via the [`MatchedCredential`].
-pub struct SimpleV0Credential<C, T>
-where
-    C: V0CryptoMaterial,
-    T: Debug + Eq,
-{
-    material: C,
-    match_data: T,
-}
-
-impl<C, T> SimpleV0Credential<C, T>
-where
-    C: V0CryptoMaterial,
-    T: Debug + Eq,
-{
-    /// Construct a new credential.
-    ///
-    /// `material` will be returned by [V0Credential::crypto_material].
-    /// `match_data` will be returned by [SimpleV0Credential::matched], wrapped in [SimpleMatchedCredential].
-    pub fn new(material: C, match_data: T) -> Self {
-        Self { material, match_data }
-    }
-}
-
-impl<C, T> MatchableCredential for SimpleV0Credential<C, T>
-where
-    C: V0CryptoMaterial,
-    T: Debug + Eq,
-{
-    type Matched<'m> = SimpleMatchedCredential<'m, T> where Self: 'm;
-
-    fn matched(&'_ self) -> Self::Matched<'_> {
-        SimpleMatchedCredential { data: &self.match_data }
-    }
-}
-
-impl<C, T> V0Credential for SimpleV0Credential<C, T>
-where
-    C: V0CryptoMaterial,
-    T: Debug + Eq,
-{
-    type CryptoMaterial = C;
-
-    fn crypto_material(&self) -> &Self::CryptoMaterial {
-        &self.material
-    }
-}
-
-/// A simple implementation of [V1Credential] that wraps a [V1CryptoMaterial] and some `T` data that
-/// will be exposed via the [MatchedCredential].
-pub struct SimpleV1Credential<C, T>
-where
-    C: V1CryptoMaterial,
-    T: Debug + Eq,
-{
-    material: C,
-    match_data: T,
-}
-
-impl<C, T> SimpleV1Credential<C, T>
-where
-    C: V1CryptoMaterial,
-    T: Debug + Eq,
-{
-    /// Construct a new credential.
-    ///
-    /// `material` will be returned by [V1Credential::crypto_material].
-    /// `match_data` will be returned by [SimpleV1Credential::matched], wrapped in [SimpleMatchedCredential].
-    pub fn new(material: C, match_data: T) -> Self {
-        Self { material, match_data }
-    }
-}
-
-impl<C, T> MatchableCredential for SimpleV1Credential<C, T>
-where
-    C: V1CryptoMaterial,
-    T: Debug + Eq,
-{
-    type Matched<'m> = SimpleMatchedCredential<'m, T> where Self: 'm;
-
-    fn matched(&'_ self) -> Self::Matched<'_> {
-        SimpleMatchedCredential { data: &self.match_data }
-    }
-}
-
-impl<C, T> V1Credential for SimpleV1Credential<C, T>
-where
-    C: V1CryptoMaterial,
-    T: Debug + Eq,
-{
-    type CryptoMaterial = C;
-
-    fn crypto_material(&self) -> &Self::CryptoMaterial {
-        &self.material
-    }
-}
-
-/// The [MatchedCredential] used by [SimpleV0Credential]
-/// and by [SimpleV1Credential].
-#[derive(Debug, PartialEq, Eq)]
-pub struct SimpleMatchedCredential<'m, T: Debug + PartialEq + Eq> {
-    data: &'m T,
-}
-
-impl<'m, T: Debug + PartialEq + Eq> SimpleMatchedCredential<'m, T> {
-    /// Construct a new instance that wraps `data`.
-    pub fn new(data: &'m T) -> Self {
-        Self { data }
-    }
-
-    /// Returns the underlying matched credential data.
-    pub fn matched_data(&self) -> &'m T {
-        self.data
-    }
-}
-
-impl<'m, T: Debug + PartialEq + Eq> MatchedCredential<'m> for SimpleMatchedCredential<'m, T> {}
diff --git a/nearby/presence/np_adv/src/credential/source.rs b/nearby/presence/np_adv/src/credential/source.rs
index e102b7e..709bbd6 100644
--- a/nearby/presence/np_adv/src/credential/source.rs
+++ b/nearby/presence/np_adv/src/credential/source.rs
@@ -12,139 +12,135 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Traits defining sources for credentials. These are used in the deserialization path to provide
-//! the credentials to try. [`SliceCredentialSource`] and [`OwnedCredentialSource`] implementations
-//! are also defined in this module.
+//! Definitions of traits for structures which supply
+//! credentials for discovering advertisements/advertisement
+//! sections for a _particular_ protocol version.
 
-use super::*;
-use alloc::vec::Vec;
+use crate::credential::{
+    DiscoveryCryptoMaterial, MatchableCredential, MatchedCredential, ProtocolVersion,
+    ReferencedMatchedCredential,
+};
+use core::borrow::Borrow;
 
-/// A source of credentials to try when decrypting advertisements,
-/// which really just wraps an iterator over a given credential type.
-pub trait CredentialSource<C: MatchableCredential> {
-    /// The iterator type produced that emits credentials
-    type Iterator<'a>: Iterator<Item = &'a C>
-    where
-        Self: 'a,
-        C: 'a;
+/// Specialized version of the [`CredentialSource`] trait for
+/// credential-sources which provide discovery credentials
+/// for a specific protocol version.
+///
+/// If you want ready-made structures which can provide
+/// credentials for both V0 and V1 protocol versions,
+/// see [`crate::credential::book::CredentialBook`]
+/// and [`crate::credential::book::CredentialBookBuilder`] instead.
+///
+/// It's preferred to use this kind of credential-source
+/// in client code, if possible, and then lift to a
+/// [`CredentialSource`] using [`AsCredentialSource`]
+/// instead of implementing [`CredentialSource`] directly,
+/// since it's better to trust this crate to handle
+/// the details of what's in [`DiscoveryCryptoMaterial`]s
+/// for specific protocol versions.
+pub trait DiscoveryCredentialSource<'a, V: ProtocolVersion>
+where
+    Self: 'a,
+{
+    /// The kind of data yielded to the caller upon a successful
+    /// identity-match.
+    type Matched: MatchedCredential;
+
+    /// The kind of crypto-material yielded from the wrapped
+    /// iterator, which allows borrowing a discovery credential.
+    type Crypto: DiscoveryCryptoMaterial<V> + Borrow<V::DiscoveryCredential>;
+
+    /// The iterator type produced which emits credentials.
+    /// This is a lending iterator which may borrow things from `self`.
+    type Iterator: Iterator<Item = (Self::Crypto, Self::Matched)>;
 
     /// Iterate over the available credentials
-    fn iter(&self) -> Self::Iterator<'_>;
+    fn iter(&'a self) -> Self::Iterator;
 }
 
-/// Trait for combined credential sources able to yield credential sources for both V0 and V1.
-pub trait BothCredentialSource<C0, C1>
+/// A source of credentials for a particular protocol version,
+/// utilizing any [`DiscoveryCryptoMaterial`] which is usable
+/// for discovering advertisements in that protocol version.
+///
+/// This trait is largely leveraged as a tool for building
+/// new kinds of [`crate::credential::book::CredentialBook`]s
+/// via the [`crate::credential::book::CredentialBookFromSources`]
+/// wrapper. It differs from the [`DiscoveryCredentialSource`]
+/// trait in that the crypto-materials do not have to be
+/// discovery credentials, and can instead be some pre-calculated
+/// crypto-materials.
+///
+/// See [`crate::credential::book::CachedCredentialSource`]
+/// for an example of this pattern.
+pub trait CredentialSource<'a, V: ProtocolVersion>
 where
-    C0: V0Credential,
-    C1: V1Credential,
+    Self: 'a,
 {
-    /// The type of the underlying credential-source for v0 credentials
-    type V0Source: CredentialSource<C0>;
-    /// The type of the underlying credential-source for v1 credentials
-    type V1Source: CredentialSource<C1>;
+    /// The kind of data yielded to the caller upon a successful
+    /// identity-match.
+    type Matched: MatchedCredential;
 
-    /// Gets a source for v0 credentials maintained by this `BothCredentialSource`.
-    fn v0(&self) -> &Self::V0Source;
+    /// The kind of crypto-material yielded from the wrapped
+    /// iterator.
+    type Crypto: DiscoveryCryptoMaterial<V>;
 
-    /// Gets a source for v1 credentials maintained by this `BothCredentialSource`.
-    fn v1(&self) -> &Self::V1Source;
+    /// The iterator type produced which emits credentials.
+    /// This is a lending iterator which may borrow things from `self`.
+    type Iterator: Iterator<Item = (Self::Crypto, Self::Matched)>;
 
-    /// Convenient function alias to [`self.v0().iter()`] for iterating
-    /// over v0 credentials.
-    fn iter_v0(&self) -> <Self::V0Source as CredentialSource<C0>>::Iterator<'_> {
-        self.v0().iter()
-    }
+    /// Iterate over the available credentials
+    fn iter(&'a self) -> Self::Iterator;
+}
 
-    /// Convenient function alias to the [`CredentialSource<C1>#iter()`] for iterating
-    /// over v0 credentials.
-    fn iter_v1(&self) -> <Self::V1Source as CredentialSource<C1>>::Iterator<'_> {
-        self.v1().iter()
+// Note: This is needed to get around coherence problems
+// with the [`CredentialSource`] trait's relationship
+// with [`DiscoveryCredentialSource`] if it were declared
+// as a sub-trait (i.e: conflicting impls)
+/// Wrapper which turns any [`DiscoveryCredentialSource`]
+/// into a [`CredentialSource`].
+pub struct AsCredentialSource<S>(pub S);
+
+impl<'a, V: ProtocolVersion, S: DiscoveryCredentialSource<'a, V>> CredentialSource<'a, V>
+    for AsCredentialSource<S>
+{
+    type Matched = <S as DiscoveryCredentialSource<'a, V>>::Matched;
+    type Crypto = <S as DiscoveryCredentialSource<'a, V>>::Crypto;
+    type Iterator = <S as DiscoveryCredentialSource<'a, V>>::Iterator;
+
+    fn iter(&'a self) -> Self::Iterator {
+        self.0.iter()
     }
 }
 
-/// A simple [CredentialSource] that just iterates over a provided slice of credentials
-pub struct SliceCredentialSource<'c, C: MatchableCredential> {
-    credentials: &'c [C],
+/// A simple [`DiscoveryCredentialSource`] which iterates over a provided slice of credentials
+pub struct SliceCredentialSource<'c, V: ProtocolVersion, M: MatchedCredential> {
+    credentials: &'c [MatchableCredential<V, M>],
 }
 
-impl<'c, C: MatchableCredential> SliceCredentialSource<'c, C> {
-    /// Construct the credential source from the provided credentials.
-    pub fn new(credentials: &'c [C]) -> Self {
+impl<'c, V: ProtocolVersion, M: MatchedCredential> SliceCredentialSource<'c, V, M> {
+    /// Construct the credential supplier from the provided slice of credentials.
+    pub fn new(credentials: &'c [MatchableCredential<V, M>]) -> Self {
         Self { credentials }
     }
 }
 
-impl<'c, C: MatchableCredential> CredentialSource<C> for SliceCredentialSource<'c, C> {
-    type Iterator<'i>  = core::slice::Iter<'i, C>
-    where Self: 'i;
-
-    fn iter(&'_ self) -> Self::Iterator<'_> {
-        self.credentials.iter()
-    }
-}
-
-/// A simple credential source which owns all of its credentials.
-pub struct OwnedCredentialSource<C: MatchableCredential> {
-    credentials: Vec<C>,
-}
-
-impl<C: MatchableCredential> OwnedCredentialSource<C> {
-    /// Constructs an owned credential source from the given credentials
-    pub fn new(credentials: Vec<C>) -> Self {
-        Self { credentials }
-    }
-}
-
-impl<C: MatchableCredential> CredentialSource<C> for OwnedCredentialSource<C> {
-    type Iterator<'i>  = core::slice::Iter<'i, C>
-    where Self: 'i;
-
-    fn iter(&'_ self) -> Self::Iterator<'_> {
-        self.credentials.iter()
-    }
-}
-
-/// An owned credential source for both v0 and v1 credentials,
-pub struct OwnedBothCredentialSource<C0, C1>
+impl<'a, 'b, V: ProtocolVersion, M: MatchedCredential> DiscoveryCredentialSource<'a, V>
+    for SliceCredentialSource<'b, V, M>
 where
-    C0: V0Credential,
-    C1: V1Credential,
+    'b: 'a,
+    Self: 'b,
+    &'a <V as ProtocolVersion>::DiscoveryCredential: DiscoveryCryptoMaterial<V>,
 {
-    v0_source: OwnedCredentialSource<C0>,
-    v1_source: OwnedCredentialSource<C1>,
-}
+    type Matched = ReferencedMatchedCredential<'a, M>;
+    type Crypto = &'a V::DiscoveryCredential;
+    type Iterator = core::iter::Map<
+        core::slice::Iter<'a, MatchableCredential<V, M>>,
+        fn(
+            &'a MatchableCredential<V, M>,
+        ) -> (&'a V::DiscoveryCredential, ReferencedMatchedCredential<M>),
+    >;
 
-impl<C0, C1> OwnedBothCredentialSource<C0, C1>
-where
-    C0: V0Credential,
-    C1: V1Credential,
-{
-    /// Creates a new `OwnedBothCredentialSource` from credential-lists
-    /// for both V0 and V1
-    pub fn new(v0_credentials: Vec<C0>, v1_credentials: Vec<C1>) -> Self {
-        let v0_source = OwnedCredentialSource::new(v0_credentials);
-        let v1_source = OwnedCredentialSource::new(v1_credentials);
-        Self { v0_source, v1_source }
-    }
-
-    /// Creates a new credential source that is empty.
-    pub fn new_empty() -> Self {
-        Self::new(Vec::new(), Vec::new())
-    }
-}
-
-impl<C0, C1> BothCredentialSource<C0, C1> for OwnedBothCredentialSource<C0, C1>
-where
-    C0: V0Credential,
-    C1: V1Credential,
-{
-    type V0Source = OwnedCredentialSource<C0>;
-    type V1Source = OwnedCredentialSource<C1>;
-
-    fn v0(&self) -> &Self::V0Source {
-        &self.v0_source
-    }
-    fn v1(&self) -> &Self::V1Source {
-        &self.v1_source
+    fn iter(&'a self) -> Self::Iterator {
+        self.credentials.iter().map(MatchableCredential::<V, M>::as_pair)
     }
 }
diff --git a/nearby/presence/np_adv/src/credential/tests.rs b/nearby/presence/np_adv/src/credential/tests.rs
new file mode 100644
index 0000000..297fdb5
--- /dev/null
+++ b/nearby/presence/np_adv/src/credential/tests.rs
@@ -0,0 +1,206 @@
+// Copyright 2023 Google LLC
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests of functionality related to credentials, credential-views, and credential suppliers.
+
+use crate::credential::{
+    book::{
+        init_cache_from_source, CachedCredentialSource, PossiblyCachedDiscoveryCryptoMaterialKind,
+    },
+    source::{CredentialSource, SliceCredentialSource},
+    v0::{V0DiscoveryCredential, V0},
+    v1::{
+        SignedBroadcastCryptoMaterial, SimpleSignedBroadcastCryptoMaterial, V1DiscoveryCredential,
+        V1DiscoveryCryptoMaterial, V1,
+    },
+    BroadcastCryptoMaterial, EmptyMatchedCredential, KeySeedMatchedCredential, MatchableCredential,
+    MetadataDecryptionError, ProtocolVersion, ReferencedMatchedCredential,
+    SimpleBroadcastCryptoMaterial,
+};
+use crate::legacy::ShortMetadataKey;
+use crate::MetadataKey;
+use alloc::{vec, vec::Vec};
+use crypto_provider_default::CryptoProviderImpl;
+
+fn get_zeroed_v0_discovery_credential() -> V0DiscoveryCredential {
+    V0DiscoveryCredential::new([0u8; 32], [0u8; 32])
+}
+
+fn get_constant_packed_v1_discovery_credential(value: u8) -> V1DiscoveryCredential {
+    let key_pair = np_ed25519::KeyPair::<CryptoProviderImpl>::generate();
+    SimpleSignedBroadcastCryptoMaterial::new(
+        [value; 32],
+        MetadataKey([value; 16]),
+        // NOTE: This winds up being unused in these test cases
+        key_pair.private_key(),
+    )
+    .derive_v1_discovery_credential::<CryptoProviderImpl>()
+}
+
+#[test]
+fn cached_credential_source_keeps_same_entries_as_original() {
+    let creds: [MatchableCredential<V1, KeySeedMatchedCredential>; 5] =
+        [0u8, 1, 2, 3, 4].map(|x| {
+            let match_data = KeySeedMatchedCredential::from([x; 32]);
+            MatchableCredential {
+                discovery_credential: get_constant_packed_v1_discovery_credential(x),
+                match_data,
+            }
+        });
+    let supplier = SliceCredentialSource::new(&creds);
+    let cache = init_cache_from_source::<_, _, 3, CryptoProviderImpl>(&supplier);
+    let cached = CachedCredentialSource::new(supplier, cache);
+    let cached_view = &cached;
+    assert_eq!(cached_view.iter().count(), 5);
+    // Now we're going to check that the pairings between the match-data
+    // and the MIC hmac key wind up being the same between the original
+    // creds list and what's provided by the cached source.
+    let expected: Vec<_> = creds
+        .iter()
+        .map(|cred| {
+            (
+                cred.discovery_credential
+                    .unsigned_verification_material::<CryptoProviderImpl>()
+                    .mic_hmac_key,
+                ReferencedMatchedCredential::from(&cred.match_data),
+            )
+        })
+        .collect();
+    let actual: Vec<_> = cached_view
+        .iter()
+        .map(|(crypto_material, match_data)| {
+            (
+                crypto_material.unsigned_verification_material::<CryptoProviderImpl>().mic_hmac_key,
+                match_data,
+            )
+        })
+        .collect();
+    assert_eq!(actual, expected);
+}
+
+#[test]
+fn cached_credential_source_has_requested_cache_size() {
+    let creds: [MatchableCredential<V0, EmptyMatchedCredential>; 10] =
+        [0u8; 10].map(|_| MatchableCredential {
+            discovery_credential: get_zeroed_v0_discovery_credential(),
+            match_data: EmptyMatchedCredential,
+        });
+    let supplier = SliceCredentialSource::new(&creds);
+    let cache = init_cache_from_source::<_, _, 5, CryptoProviderImpl>(&supplier);
+    let cached = CachedCredentialSource::new(supplier, cache);
+    let cached_view = &cached;
+    assert_eq!(cached_view.iter().count(), 10);
+    for (i, (cred, _)) in cached_view.iter().enumerate() {
+        if i < 5 {
+            // Should be cached
+            if let PossiblyCachedDiscoveryCryptoMaterialKind::Precalculated(_) = cred.wrapped {
+            } else {
+                panic!("Credential #{} was not cached", i);
+            }
+        } else {
+            // Should be discovery credentials
+            if let PossiblyCachedDiscoveryCryptoMaterialKind::Discovery(_) = cred.wrapped {
+            } else {
+                panic!("Credential #{} was not supposed to be cached", i);
+            }
+        }
+    }
+}
+
+#[test]
+fn v0_metadata_decryption_works_same_metadata_key() {
+    let key_seed = [3u8; 32];
+    let metadata_key = ShortMetadataKey([5u8; 14]);
+
+    let metadata = vec![7u8; 42];
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, metadata_key);
+
+    let encrypted_metadata = broadcast_cm.encrypt_metadata::<CryptoProviderImpl>(&metadata);
+
+    let metadata_nonce = broadcast_cm.metadata_nonce::<CryptoProviderImpl>();
+
+    let decryption_result = V0::decrypt_metadata::<CryptoProviderImpl>(
+        metadata_nonce,
+        metadata_key,
+        &encrypted_metadata,
+    );
+    assert_eq!(decryption_result, Ok(metadata))
+}
+
+#[test]
+fn v1_metadata_decryption_works_same_metadata_key() {
+    let key_seed = [9u8; 32];
+    let metadata_key = MetadataKey([2u8; 16]);
+
+    let metadata = vec![6u8; 51];
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
+    let encrypted_metadata = broadcast_cm.encrypt_metadata::<CryptoProviderImpl>(&metadata);
+
+    let metadata_nonce = broadcast_cm.metadata_nonce::<CryptoProviderImpl>();
+
+    let decryption_result = V1::decrypt_metadata::<CryptoProviderImpl>(
+        metadata_nonce,
+        metadata_key,
+        &encrypted_metadata,
+    );
+    assert_eq!(decryption_result, Ok(metadata))
+}
+
+#[test]
+fn v0_metadata_decryption_fails_different_metadata_key() {
+    let key_seed = [3u8; 32];
+    let encrypting_metadata_key = ShortMetadataKey([5u8; 14]);
+
+    let metadata = vec![7u8; 42];
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, encrypting_metadata_key);
+
+    let encrypted_metadata = broadcast_cm.encrypt_metadata::<CryptoProviderImpl>(&metadata);
+
+    let metadata_nonce = broadcast_cm.metadata_nonce::<CryptoProviderImpl>();
+
+    let decrypting_metadata_key = ShortMetadataKey([6u8; 14]);
+
+    let decryption_result = V0::decrypt_metadata::<CryptoProviderImpl>(
+        metadata_nonce,
+        decrypting_metadata_key,
+        &encrypted_metadata,
+    );
+    assert_eq!(decryption_result, Err(MetadataDecryptionError))
+}
+
+#[test]
+fn v1_metadata_decryption_fails_different_metadata_key() {
+    let key_seed = [251u8; 32];
+    let encrypting_metadata_key = MetadataKey([127u8; 16]);
+
+    let metadata = vec![255u8; 42];
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, encrypting_metadata_key);
+
+    let encrypted_metadata = broadcast_cm.encrypt_metadata::<CryptoProviderImpl>(&metadata);
+
+    let metadata_nonce = broadcast_cm.metadata_nonce::<CryptoProviderImpl>();
+
+    let decrypting_metadata_key = MetadataKey([249u8; 16]);
+
+    let decryption_result = V1::decrypt_metadata::<CryptoProviderImpl>(
+        metadata_nonce,
+        decrypting_metadata_key,
+        &encrypted_metadata,
+    );
+    assert_eq!(decryption_result, Err(MetadataDecryptionError))
+}
diff --git a/nearby/presence/np_adv/src/credential/v0.rs b/nearby/presence/np_adv/src/credential/v0.rs
index e2d1b5d..c89909d 100644
--- a/nearby/presence/np_adv/src/credential/v0.rs
+++ b/nearby/presence/np_adv/src/credential/v0.rs
@@ -13,32 +13,33 @@
 // limitations under the License.
 
 //! Cryptographic materials for v0 advertisement-format credentials.
+use crate::credential::{protocol_version_seal, DiscoveryCryptoMaterial, ProtocolVersion};
+use crate::legacy::ShortMetadataKey;
+use crate::MetadataKey;
+use crypto_provider::{CryptoProvider, CryptoRng};
 
-use super::*;
-
-/// Cryptographic material for an individual NP credential used to decrypt v0 advertisements.
-// Space-time tradeoffs:
-// - LDT keys (64b) take about 1.4us.
-pub trait V0CryptoMaterial {
-    /// Returns an LDT NP advertisement cipher built with the provided `Aes`
-    fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C>;
-}
-
-/// A [`V0CryptoMaterial`] that minimizes memory footprint at the expense of CPU time when
-/// providing derived key material
-pub struct MinimumFootprintV0CryptoMaterial {
+/// Cryptographic information about a particular V0 discovery credential
+/// necessary to match and decrypt encrypted V0 advertisements.
+#[derive(Clone)]
+pub struct V0DiscoveryCredential {
     key_seed: [u8; 32],
     legacy_metadata_key_hmac: [u8; 32],
 }
 
-impl MinimumFootprintV0CryptoMaterial {
-    /// Construct an [MinimumFootprintV0CryptoMaterial] from the provided identity data.
+impl V0DiscoveryCredential {
+    /// Construct an [V0DiscoveryCredential] from the provided identity data.
     pub fn new(key_seed: [u8; 32], legacy_metadata_key_hmac: [u8; 32]) -> Self {
         Self { key_seed, legacy_metadata_key_hmac }
     }
 }
 
-impl V0CryptoMaterial for MinimumFootprintV0CryptoMaterial {
+impl DiscoveryCryptoMaterial<V0> for V0DiscoveryCredential {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        V0::metadata_nonce_from_key_seed::<C>(&self.key_seed)
+    }
+}
+
+impl V0DiscoveryCryptoMaterial for V0DiscoveryCredential {
     fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C> {
         let hkdf = np_hkdf::NpKeySeedHkdf::new(&self.key_seed);
         ldt_np_adv::build_np_adv_decrypter(
@@ -49,27 +50,73 @@
     }
 }
 
-/// [`V0CryptoMaterial`] that minimizes CPU time when providing key material at
+/// Type-level identifier for the V0 protocol version.
+#[derive(Debug, Clone)]
+pub enum V0 {}
+
+impl protocol_version_seal::ProtocolVersionSeal for V0 {}
+
+impl ProtocolVersion for V0 {
+    type DiscoveryCredential = V0DiscoveryCredential;
+    type MetadataKey = ShortMetadataKey;
+
+    fn metadata_nonce_from_key_seed<C: CryptoProvider>(key_seed: &[u8; 32]) -> [u8; 12] {
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(key_seed);
+        hkdf.legacy_metadata_nonce()
+    }
+    fn expand_metadata_key<C: CryptoProvider>(metadata_key: ShortMetadataKey) -> MetadataKey {
+        metadata_key.expand::<C>()
+    }
+    fn gen_random_metadata_key<R: CryptoRng>(rng: &mut R) -> ShortMetadataKey {
+        ShortMetadataKey(rng.gen())
+    }
+}
+
+/// Trait which exists purely to be able to restrict the protocol
+/// version of certain type-bounds to V0.
+pub trait V0ProtocolVersion: ProtocolVersion {}
+
+impl V0ProtocolVersion for V0 {}
+
+/// Cryptographic material for an individual NP credential used to decrypt v0 advertisements.
+/// Unlike [`V0DiscoveryCredential`], derived information about cryptographic materials may
+/// be stored in implementors of this trait.
+// Space-time tradeoffs:
+// - LDT keys (64b) take about 1.4us.
+pub trait V0DiscoveryCryptoMaterial: DiscoveryCryptoMaterial<V0> {
+    /// Returns an LDT NP advertisement cipher built with the provided `Aes`
+    fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C>;
+}
+
+/// [`V0DiscoveryCryptoMaterial`] that minimizes CPU time when providing key material at
 /// the expense of occupied memory.
-pub struct PrecalculatedV0CryptoMaterial {
+pub struct PrecalculatedV0DiscoveryCryptoMaterial {
     pub(crate) legacy_ldt_key: ldt::LdtKey<xts_aes::XtsAes128Key>,
     pub(crate) legacy_metadata_key_hmac: [u8; 32],
     pub(crate) legacy_metadata_key_hmac_key: [u8; 32],
+    pub(crate) metadata_nonce: [u8; 12],
 }
 
-impl PrecalculatedV0CryptoMaterial {
+impl PrecalculatedV0DiscoveryCryptoMaterial {
     /// Construct a new instance from the provided credential material.
-    pub fn new<C: CryptoProvider>(key_seed: &[u8; 32], legacy_metadata_key_hmac: [u8; 32]) -> Self {
-        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(key_seed);
+    pub(crate) fn new<C: CryptoProvider>(discovery_credential: &V0DiscoveryCredential) -> Self {
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&discovery_credential.key_seed);
         Self {
             legacy_ldt_key: hkdf.legacy_ldt_key(),
-            legacy_metadata_key_hmac,
+            legacy_metadata_key_hmac: discovery_credential.legacy_metadata_key_hmac,
             legacy_metadata_key_hmac_key: *hkdf.legacy_metadata_key_hmac_key().as_bytes(),
+            metadata_nonce: hkdf.legacy_metadata_nonce(),
         }
     }
 }
 
-impl V0CryptoMaterial for PrecalculatedV0CryptoMaterial {
+impl DiscoveryCryptoMaterial<V0> for PrecalculatedV0DiscoveryCryptoMaterial {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        self.metadata_nonce
+    }
+}
+
+impl V0DiscoveryCryptoMaterial for PrecalculatedV0DiscoveryCryptoMaterial {
     fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C> {
         ldt_np_adv::build_np_adv_decrypter(
             &self.legacy_ldt_key,
@@ -78,3 +125,30 @@
         )
     }
 }
+
+// Implementations for reference types -- we don't provide a blanket impl for references
+// due to the potential to conflict with downstream crates' implementations.
+
+impl<'a> DiscoveryCryptoMaterial<V0> for &'a V0DiscoveryCredential {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        (*self).metadata_nonce::<C>()
+    }
+}
+
+impl<'a> V0DiscoveryCryptoMaterial for &'a V0DiscoveryCredential {
+    fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C> {
+        (*self).ldt_adv_cipher::<C>()
+    }
+}
+
+impl<'a> DiscoveryCryptoMaterial<V0> for &'a PrecalculatedV0DiscoveryCryptoMaterial {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        (*self).metadata_nonce::<C>()
+    }
+}
+
+impl<'a> V0DiscoveryCryptoMaterial for &'a PrecalculatedV0DiscoveryCryptoMaterial {
+    fn ldt_adv_cipher<C: CryptoProvider>(&self) -> ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C> {
+        (*self).ldt_adv_cipher::<C>()
+    }
+}
diff --git a/nearby/presence/np_adv/src/credential/v1.rs b/nearby/presence/np_adv/src/credential/v1.rs
index 4b134e4..7d060f2 100644
--- a/nearby/presence/np_adv/src/credential/v1.rs
+++ b/nearby/presence/np_adv/src/credential/v1.rs
@@ -14,17 +14,133 @@
 
 //! Cryptographic materials for v1 advertisement-format credentials.
 
-use core::borrow::Borrow;
-use crypto_provider::{aes::Aes128Key, ed25519, CryptoProvider};
+use crate::credential::{
+    protocol_version_seal, BroadcastCryptoMaterial, DiscoveryCryptoMaterial, ProtocolVersion,
+};
+use crate::MetadataKey;
+use crypto_provider::{aes::Aes128Key, ed25519, CryptoProvider, CryptoRng};
 use np_hkdf::UnsignedSectionKeys;
 
+/// Cryptographic information about a particular V1 discovery credential
+/// necessary to match and decrypt encrypted V1 sections.
+#[derive(Clone)]
+pub struct V1DiscoveryCredential {
+    key_seed: [u8; 32],
+    expected_unsigned_metadata_key_hmac: [u8; 32],
+    expected_signed_metadata_key_hmac: [u8; 32],
+    pub_key: ed25519::RawPublicKey,
+}
+impl V1DiscoveryCredential {
+    /// Construct a V1 discovery credential from the provided identity data.
+    pub fn new<C: CryptoProvider>(
+        key_seed: [u8; 32],
+        expected_unsigned_metadata_key_hmac: [u8; 32],
+        expected_signed_metadata_key_hmac: [u8; 32],
+        pub_key: np_ed25519::PublicKey<C>,
+    ) -> Self {
+        Self {
+            key_seed,
+            expected_unsigned_metadata_key_hmac,
+            expected_signed_metadata_key_hmac,
+            pub_key: pub_key.to_bytes(),
+        }
+    }
+
+    /// Constructs pre-calculated crypto material from this discovery credential.
+    pub(crate) fn to_precalculated<C: CryptoProvider>(
+        &self,
+    ) -> PrecalculatedV1DiscoveryCryptoMaterial {
+        let signed_identity_resolution_material = self.signed_identity_resolution_material::<C>();
+        let unsigned_identity_resolution_material =
+            self.unsigned_identity_resolution_material::<C>();
+        let signed_verification_material = self.signed_verification_material::<C>();
+        let unsigned_verification_material = self.unsigned_verification_material::<C>();
+        let metadata_nonce = self.metadata_nonce::<C>();
+        PrecalculatedV1DiscoveryCryptoMaterial {
+            signed_identity_resolution_material,
+            unsigned_identity_resolution_material,
+            signed_verification_material,
+            unsigned_verification_material,
+            metadata_nonce,
+        }
+    }
+}
+
+impl DiscoveryCryptoMaterial<V1> for V1DiscoveryCredential {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        V1::metadata_nonce_from_key_seed::<C>(&self.key_seed)
+    }
+}
+
+impl V1DiscoveryCryptoMaterial for V1DiscoveryCredential {
+    fn signed_identity_resolution_material<C: CryptoProvider>(
+        &self,
+    ) -> SignedSectionIdentityResolutionMaterial {
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed);
+        SignedSectionIdentityResolutionMaterial::from_hkdf_and_expected_metadata_key_hmac(
+            &hkdf,
+            self.expected_signed_metadata_key_hmac,
+        )
+    }
+
+    fn unsigned_identity_resolution_material<C: CryptoProvider>(
+        &self,
+    ) -> UnsignedSectionIdentityResolutionMaterial {
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed);
+        UnsignedSectionIdentityResolutionMaterial::from_hkdf_and_expected_metadata_key_hmac(
+            &hkdf,
+            self.expected_unsigned_metadata_key_hmac,
+        )
+    }
+
+    fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
+        SignedSectionVerificationMaterial { pub_key: self.pub_key }
+    }
+
+    fn unsigned_verification_material<C: CryptoProvider>(
+        &self,
+    ) -> UnsignedSectionVerificationMaterial {
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed);
+        let mic_hmac_key = *UnsignedSectionKeys::hmac_key(&hkdf).as_bytes();
+        UnsignedSectionVerificationMaterial { mic_hmac_key }
+    }
+}
+
+/// Type-level identifier for the V1 protocol version.
+#[derive(Debug, Clone)]
+pub enum V1 {}
+
+impl protocol_version_seal::ProtocolVersionSeal for V1 {}
+
+impl ProtocolVersion for V1 {
+    type DiscoveryCredential = V1DiscoveryCredential;
+    type MetadataKey = MetadataKey;
+
+    fn metadata_nonce_from_key_seed<C: CryptoProvider>(key_seed: &[u8; 32]) -> [u8; 12] {
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(key_seed);
+        hkdf.extended_metadata_nonce()
+    }
+    fn expand_metadata_key<C: CryptoProvider>(metadata_key: MetadataKey) -> MetadataKey {
+        metadata_key
+    }
+    fn gen_random_metadata_key<R: CryptoRng>(rng: &mut R) -> MetadataKey {
+        MetadataKey(rng.gen())
+    }
+}
+
+/// Trait which exists purely to be able to restrict the protocol
+/// version of certain type-bounds to V1.
+pub trait V1ProtocolVersion: ProtocolVersion {}
+
+impl V1ProtocolVersion for V1 {}
+
 /// Cryptographic materials necessary for determining whether or not
 /// a given V1 advertisement section matches an identity.
 /// Per the construction of the V1 specification, this is also
 /// the information necessary to decrypt the raw byte contents
 /// of an encrypted V1 section.
 #[derive(Clone)]
-pub struct SectionIdentityResolutionMaterial {
+pub(crate) struct SectionIdentityResolutionMaterial {
     /// The AES key for decrypting section ciphertext
     pub(crate) aes_key: Aes128Key,
     /// The metadata key HMAC key for deriving and verifying the identity metadata
@@ -44,9 +160,21 @@
     pub(crate) fn from_raw(raw: SectionIdentityResolutionMaterial) -> Self {
         Self(raw)
     }
+    /// Extracts the underlying section-identity resolution material carried around
+    /// within this wrapper for resolution of signed sections.
+    pub(crate) fn into_raw_resolution_material(self) -> SectionIdentityResolutionMaterial {
+        self.0
+    }
+    #[cfg(any(test, feature = "devtools"))]
+    /// Gets the underlying section-identity resolution material carried around
+    /// within this wrapper for resolution of signed sections.
     pub(crate) fn as_raw_resolution_material(&self) -> &SectionIdentityResolutionMaterial {
         &self.0
     }
+
+    /// Constructs identity-resolution material for a signed section whose
+    /// discovery credential leverages the provided HKDF and has the given
+    /// expected metadata-key HMAC.
     pub(crate) fn from_hkdf_and_expected_metadata_key_hmac<C: CryptoProvider>(
         hkdf: &np_hkdf::NpKeySeedHkdf<C>,
         expected_metadata_key_hmac: [u8; 32],
@@ -69,9 +197,20 @@
     pub(crate) fn from_raw(raw: SectionIdentityResolutionMaterial) -> Self {
         Self(raw)
     }
+    /// Extracts the underlying section-identity resolution material carried around
+    /// within this wrapper for resolution of unsigned sections.
+    pub(crate) fn into_raw_resolution_material(self) -> SectionIdentityResolutionMaterial {
+        self.0
+    }
+    /// Gets the underlying section-identity resolution material carried around
+    /// within this wrapper for resolution of unsigned sections.
+    #[cfg(any(test, feature = "devtools"))]
     pub(crate) fn as_raw_resolution_material(&self) -> &SectionIdentityResolutionMaterial {
         &self.0
     }
+    /// Constructs identity-resolution material for an unsigned (MIC) section whose
+    /// discovery credential leverages the provided HKDF and has the given
+    /// expected metadata-key HMAC.
     pub(crate) fn from_hkdf_and_expected_metadata_key_hmac<C: CryptoProvider>(
         hkdf: &np_hkdf::NpKeySeedHkdf<C>,
         expected_metadata_key_hmac: [u8; 32],
@@ -129,88 +268,54 @@
 // is only used on the matching identity, not all identities.
 
 /// Cryptographic material for an individual NP credential used to decrypt and verify v1 sections.
-pub trait V1CryptoMaterial {
-    /// The return type of `Self::signed_identity_resolution_material`, which is some
-    /// data-type which allows borrowing `SignedSectionIdentityResolutionMaterial`
-    type SignedIdentityResolverReference<'a>: Borrow<SignedSectionIdentityResolutionMaterial>
-    where
-        Self: 'a;
-    /// The return type of `Self::unsigned_identity_resolution_material`, which is some
-    /// data-type which allows borrowing `UnsignedSectionIdentityResolutionMaterial`
-    type UnsignedIdentityResolverReference<'a>: Borrow<UnsignedSectionIdentityResolutionMaterial>
-    where
-        Self: 'a;
-
-    /// Constructs or references the identity resolution material for signed sections
+pub trait V1DiscoveryCryptoMaterial: DiscoveryCryptoMaterial<V1> {
+    /// Constructs or copies the identity resolution material for signed sections
     fn signed_identity_resolution_material<C: CryptoProvider>(
         &self,
-    ) -> Self::SignedIdentityResolverReference<'_>;
+    ) -> SignedSectionIdentityResolutionMaterial;
 
-    /// Constructs or references the identity resolution material for unsigned sections
+    /// Constructs or copies the identity resolution material for unsigned sections
     fn unsigned_identity_resolution_material<C: CryptoProvider>(
         &self,
-    ) -> Self::UnsignedIdentityResolverReference<'_>;
+    ) -> UnsignedSectionIdentityResolutionMaterial;
 
     /// Constructs or copies non-identity-resolution deserialization material for signed
     /// sections.
-    ///
-    /// Note: We mandate "copies" here due to the relatively small size of verification-only crypto
-    /// materials (32 bytes).
     fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial;
 
     /// Constructs or copies non-identity-resolution deserialization material for unsigned
     /// sections.
-    ///
-    /// Note: We mandate "copies" here due to the relatively small size of verification-only crypto
-    /// materials (32 bytes).
     fn unsigned_verification_material<C: CryptoProvider>(
         &self,
     ) -> UnsignedSectionVerificationMaterial;
-
-    /// Constructs pre-calculated crypto material out of this crypto-material.
-    fn to_precalculated<C: CryptoProvider>(self) -> PrecalculatedV1CryptoMaterial
-    where
-        Self: Sized,
-    {
-        let signed_identity_resolution_material =
-            self.signed_identity_resolution_material::<C>().borrow().clone();
-        let unsigned_identity_resolution_material =
-            self.unsigned_identity_resolution_material::<C>().borrow().clone();
-        let signed_verification_material = self.signed_verification_material::<C>();
-        let unsigned_verification_material = self.unsigned_verification_material::<C>();
-        PrecalculatedV1CryptoMaterial {
-            signed_identity_resolution_material,
-            unsigned_identity_resolution_material,
-            signed_verification_material,
-            unsigned_verification_material,
-        }
-    }
 }
 
-/// [`V1CryptoMaterial`] that minimizes CPU time when providing key material at
+/// V1 [`DiscoveryCryptoMaterial`] that minimizes CPU time when providing key material at
 /// the expense of occupied memory
-pub struct PrecalculatedV1CryptoMaterial {
+pub struct PrecalculatedV1DiscoveryCryptoMaterial {
     pub(crate) signed_identity_resolution_material: SignedSectionIdentityResolutionMaterial,
     pub(crate) unsigned_identity_resolution_material: UnsignedSectionIdentityResolutionMaterial,
     pub(crate) signed_verification_material: SignedSectionVerificationMaterial,
     pub(crate) unsigned_verification_material: UnsignedSectionVerificationMaterial,
+    pub(crate) metadata_nonce: [u8; 12],
 }
 
-impl V1CryptoMaterial for PrecalculatedV1CryptoMaterial {
-    type SignedIdentityResolverReference<'a> = &'a SignedSectionIdentityResolutionMaterial
-        where Self: 'a;
-    type UnsignedIdentityResolverReference<'a> = &'a UnsignedSectionIdentityResolutionMaterial
-        where Self: 'a;
+impl DiscoveryCryptoMaterial<V1> for PrecalculatedV1DiscoveryCryptoMaterial {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        self.metadata_nonce
+    }
+}
 
+impl V1DiscoveryCryptoMaterial for PrecalculatedV1DiscoveryCryptoMaterial {
     fn signed_identity_resolution_material<C: CryptoProvider>(
         &self,
-    ) -> Self::SignedIdentityResolverReference<'_> {
-        &self.signed_identity_resolution_material
+    ) -> SignedSectionIdentityResolutionMaterial {
+        self.signed_identity_resolution_material.clone()
     }
     fn unsigned_identity_resolution_material<C: CryptoProvider>(
         &self,
-    ) -> Self::UnsignedIdentityResolverReference<'_> {
-        &self.unsigned_identity_resolution_material
+    ) -> UnsignedSectionIdentityResolutionMaterial {
+        self.unsigned_identity_resolution_material.clone()
     }
     fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
         self.signed_verification_material.clone()
@@ -222,67 +327,126 @@
     }
 }
 
-/// [`V1CryptoMaterial`] that minimizes memory footprint at the expense of CPU
-/// time when providing derived key material.
-pub struct MinimumFootprintV1CryptoMaterial {
-    key_seed: [u8; 32],
-    expected_unsigned_metadata_key_hmac: [u8; 32],
-    expected_signed_metadata_key_hmac: [u8; 32],
-    pub_key: ed25519::RawPublicKey,
-}
+// Implementations for reference types -- we don't provide a blanket impl for references
+// due to the potential to conflict with downstream crates' implementations.
 
-impl MinimumFootprintV1CryptoMaterial {
-    /// Construct an [MinimumFootprintV1CryptoMaterial] from the provided identity data.
-    pub fn new<C: CryptoProvider>(
-        key_seed: [u8; 32],
-        expected_unsigned_metadata_key_hmac: [u8; 32],
-        expected_signed_metadata_key_hmac: [u8; 32],
-        pub_key: np_ed25519::PublicKey<C>,
-    ) -> Self {
-        Self {
-            key_seed,
-            expected_unsigned_metadata_key_hmac,
-            expected_signed_metadata_key_hmac,
-            pub_key: pub_key.to_bytes(),
-        }
+impl<'a> DiscoveryCryptoMaterial<V1> for &'a V1DiscoveryCredential {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        (*self).metadata_nonce::<C>()
     }
 }
 
-impl V1CryptoMaterial for MinimumFootprintV1CryptoMaterial {
-    type SignedIdentityResolverReference<'a> = SignedSectionIdentityResolutionMaterial
-        where Self: 'a;
-    type UnsignedIdentityResolverReference<'a> = UnsignedSectionIdentityResolutionMaterial
-        where Self: 'a;
-
+impl<'a> V1DiscoveryCryptoMaterial for &'a V1DiscoveryCredential {
     fn signed_identity_resolution_material<C: CryptoProvider>(
         &self,
-    ) -> Self::SignedIdentityResolverReference<'_> {
-        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed);
-        SignedSectionIdentityResolutionMaterial::from_hkdf_and_expected_metadata_key_hmac(
-            &hkdf,
-            self.expected_signed_metadata_key_hmac,
-        )
+    ) -> SignedSectionIdentityResolutionMaterial {
+        (*self).signed_identity_resolution_material::<C>()
     }
-
     fn unsigned_identity_resolution_material<C: CryptoProvider>(
         &self,
-    ) -> Self::UnsignedIdentityResolverReference<'_> {
-        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed);
-        UnsignedSectionIdentityResolutionMaterial::from_hkdf_and_expected_metadata_key_hmac(
-            &hkdf,
-            self.expected_unsigned_metadata_key_hmac,
-        )
+    ) -> UnsignedSectionIdentityResolutionMaterial {
+        (*self).unsigned_identity_resolution_material::<C>()
     }
-
     fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
-        SignedSectionVerificationMaterial { pub_key: self.pub_key }
+        (*self).signed_verification_material::<C>()
     }
-
     fn unsigned_verification_material<C: CryptoProvider>(
         &self,
     ) -> UnsignedSectionVerificationMaterial {
-        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed);
-        let mic_hmac_key = *UnsignedSectionKeys::hmac_key(&hkdf).as_bytes();
-        UnsignedSectionVerificationMaterial { mic_hmac_key }
+        (*self).unsigned_verification_material::<C>()
+    }
+}
+
+impl<'a> DiscoveryCryptoMaterial<V1> for &'a PrecalculatedV1DiscoveryCryptoMaterial {
+    fn metadata_nonce<C: CryptoProvider>(&self) -> [u8; 12] {
+        (*self).metadata_nonce::<C>()
+    }
+}
+
+impl<'a> V1DiscoveryCryptoMaterial for &'a PrecalculatedV1DiscoveryCryptoMaterial {
+    fn signed_identity_resolution_material<C: CryptoProvider>(
+        &self,
+    ) -> SignedSectionIdentityResolutionMaterial {
+        (*self).signed_identity_resolution_material::<C>()
+    }
+    fn unsigned_identity_resolution_material<C: CryptoProvider>(
+        &self,
+    ) -> UnsignedSectionIdentityResolutionMaterial {
+        (*self).unsigned_identity_resolution_material::<C>()
+    }
+    fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
+        (*self).signed_verification_material::<C>()
+    }
+    fn unsigned_verification_material<C: CryptoProvider>(
+        &self,
+    ) -> UnsignedSectionVerificationMaterial {
+        (*self).unsigned_verification_material::<C>()
+    }
+}
+
+/// Extension of [`BroadcastCryptoMaterial`] for `V1` to add
+/// crypto-materials which are necessary to sign V1 sections.
+pub trait SignedBroadcastCryptoMaterial: BroadcastCryptoMaterial<V1> {
+    /// Gets the advertisement-signing private key for constructing
+    /// signature-verified V1 sections.
+    ///
+    /// The private key is returned in an opaque, crypto-provider-independent
+    /// form to provide a safeguard against leaking the bytes of the key.
+    fn signing_key(&self) -> ed25519::PrivateKey;
+
+    /// Constructs the V1 discovery credential which may be used to discover
+    /// V1 advertisement sections broadcasted using this broadcast crypto-material
+    fn derive_v1_discovery_credential<C: CryptoProvider>(&self) -> V1DiscoveryCredential {
+        let key_seed = self.key_seed();
+        let metadata_key = self.metadata_key();
+        let pub_key = self.signing_key().derive_public_key::<C::Ed25519>();
+        let pub_key = np_ed25519::PublicKey::<C>::new(pub_key);
+
+        let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&key_seed);
+        let unsigned = hkdf
+            .extended_unsigned_metadata_key_hmac_key()
+            .calculate_hmac(metadata_key.0.as_slice());
+        let signed =
+            hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(metadata_key.0.as_slice());
+        V1DiscoveryCredential::new(key_seed, unsigned, signed, pub_key)
+    }
+}
+
+/// Concrete implementation of a [`SignedBroadcastCryptoMaterial`] which keeps the key
+/// seed, the V1 metadata key, and the signing key contiguous in memory.
+///
+/// For more flexible expression of broadcast
+/// credentials, feel free to directly implement [`SignedBroadcastCryptoMaterial`]
+/// for your own broadcast-credential-storing data-type.
+pub struct SimpleSignedBroadcastCryptoMaterial {
+    key_seed: [u8; 32],
+    metadata_key: MetadataKey,
+    signing_key: ed25519::PrivateKey,
+}
+
+impl SimpleSignedBroadcastCryptoMaterial {
+    /// Builds some simple V1 signed broadcast crypto-materials out of
+    /// the provided key-seed, metadata-key, and signing key.
+    pub fn new(
+        key_seed: [u8; 32],
+        metadata_key: MetadataKey,
+        signing_key: ed25519::PrivateKey,
+    ) -> Self {
+        Self { key_seed, metadata_key, signing_key }
+    }
+}
+
+impl BroadcastCryptoMaterial<V1> for SimpleSignedBroadcastCryptoMaterial {
+    fn key_seed(&self) -> [u8; 32] {
+        self.key_seed
+    }
+    fn metadata_key(&self) -> MetadataKey {
+        self.metadata_key
+    }
+}
+
+impl SignedBroadcastCryptoMaterial for SimpleSignedBroadcastCryptoMaterial {
+    fn signing_key(&self) -> ed25519::PrivateKey {
+        self.signing_key.clone()
     }
 }
diff --git a/nearby/presence/np_adv/src/de_type.rs b/nearby/presence/np_adv/src/de_type.rs
index 90c86e1..4966ed0 100644
--- a/nearby/presence/np_adv/src/de_type.rs
+++ b/nearby/presence/np_adv/src/de_type.rs
@@ -29,7 +29,7 @@
 ///
 /// Shared between V0 and V1.
 ///
-/// Must not overlap with [PlainDataElementType].
+/// Must not overlap with [`PlainDataElementType`](crate::legacy::de_type::PlainDataElementType).
 #[derive(strum_macros::EnumIter, Debug, Clone, Copy, PartialEq, Eq)]
 #[allow(clippy::enum_variant_names)]
 pub(crate) enum IdentityDataElementType {
diff --git a/nearby/presence/np_adv/src/deser_v0_tests.rs b/nearby/presence/np_adv/src/deser_v0_tests.rs
index fa94b89..2fb6f38 100644
--- a/nearby/presence/np_adv/src/deser_v0_tests.rs
+++ b/nearby/presence/np_adv/src/deser_v0_tests.rs
@@ -18,28 +18,30 @@
 
 use crate::{
     credential::{
-        simple::{SimpleMatchedCredential, SimpleV0Credential},
-        source::SliceCredentialSource,
-        v0::MinimumFootprintV0CryptoMaterial,
+        book::{CredentialBook, CredentialBookBuilder},
+        v0::{V0DiscoveryCredential, V0},
+        EmptyMatchedCredential, MatchableCredential, MatchedCredential,
+        SimpleBroadcastCryptoMaterial,
     },
     de_type::EncryptedIdentityDataElementType,
-    deserialize_v0_advertisement,
+    deserialization_arena,
+    deserialization_arena::DeserializationArena,
+    deserialize_advertisement,
     legacy::{
         actions::{ActionBits, ActionsDataElement, ToActionElement},
         data_elements::DataElement,
         deserialize::PlainDataElement,
         serialize::{AdvBuilder, Identity, LdtIdentity},
-        BLE_ADV_SVC_CONTENT_LEN,
+        ShortMetadataKey, BLE_ADV_SVC_CONTENT_LEN,
     },
     shared_data::ContextSyncSeqNum,
-    CredentialSource, NoIdentity, PlaintextIdentityMode, PublicIdentity, V0AdvContents,
-    V0Credential,
+    HasIdentityMatch, PlaintextIdentityMode, PublicIdentity, V0AdvertisementContents,
 };
 use array_view::ArrayView;
-use core::marker::PhantomData;
+use core::{borrow::BorrowMut, marker::PhantomData};
 use crypto_provider::CryptoProvider;
 use crypto_provider_default::CryptoProviderImpl;
-use ldt_np_adv::{LdtEncrypterXtsAes128, LegacySalt};
+use ldt_np_adv::LegacySalt;
 use std::{prelude::rust_2021::*, vec};
 use strum::IntoEnumIterator as _;
 
@@ -51,10 +53,18 @@
 
         let (adv, adv_config) = adv_random_identity(&mut rng, &identities);
 
-        let creds = identities.iter().map(|i| i.credential()).collect::<Vec<_>>();
-        let cred_source = SliceCredentialSource::new(&creds);
+        let creds = identities
+            .iter()
+            .map(|i| MatchableCredential {
+                discovery_credential: i.discovery_credential(),
+                match_data: EmptyMatchedCredential,
+            })
+            .collect::<Vec<_>>();
 
-        let contents = deser_v0::<_, _, CryptoProviderImpl>(&cred_source, adv.as_slice());
+        let mut arena = deserialization_arena!();
+        let cred_book =
+            CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&creds, &[]);
+        let contents = deser_v0::<_, CryptoProviderImpl>(&mut arena, adv.as_slice(), &cred_book);
 
         assert_adv_equals(&adv_config, &contents);
     }
@@ -68,24 +78,31 @@
 
         let (adv, adv_config) = adv_random_identity(&mut rng, &identities);
 
-        let creds = identities
+        let credentials = identities
             .iter()
             .filter(|i| {
                 // remove identity used, if any
                 !adv_config.identity.map(|sci| sci.key_seed == i.key_seed).unwrap_or(false)
             })
-            .map(|i| i.credential())
+            .map(|i| MatchableCredential {
+                discovery_credential: i.discovery_credential(),
+                match_data: EmptyMatchedCredential,
+            })
             .collect::<Vec<_>>();
-        let cred_source = SliceCredentialSource::new(&creds);
 
-        let contents = deser_v0::<_, _, CryptoProviderImpl>(&cred_source, adv.as_slice());
+        let mut arena = deserialization_arena!();
+        let cred_book = CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(
+            &credentials,
+            &[],
+        );
+        let contents = deser_v0::<_, CryptoProviderImpl>(&mut arena, adv.as_slice(), &cred_book);
 
         match adv_config.identity {
             // we ended up generating plaintext, so it's fine
             None => assert_adv_equals(&adv_config, &contents),
             Some(_) => {
                 // we generated an encrypted adv, but didn't include the credential
-                assert_eq!(V0AdvContents::NoMatchingCredentials, contents);
+                assert_eq!(V0AdvertisementContents::NoMatchingCredentials, contents);
             }
         }
     }
@@ -99,29 +116,34 @@
 
         let (adv, adv_config) = adv_random_identity(&mut rng, &identities);
 
-        let creds = Vec::<SimpleV0Credential<MinimumFootprintV0CryptoMaterial, [u8; 32]>>::new();
-        let cred_source = SliceCredentialSource::new(&creds);
+        let creds = Vec::<MatchableCredential<V0, EmptyMatchedCredential>>::new();
 
-        let contents = deser_v0::<_, _, CryptoProviderImpl>(&cred_source, adv.as_slice());
+        let mut arena = deserialization_arena!();
+        let cred_book =
+            CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&creds, &[]);
+        let contents = deser_v0::<_, CryptoProviderImpl>(&mut arena, adv.as_slice(), &cred_book);
 
         match adv_config.identity {
             // we ended up generating plaintext, so it's fine
             None => assert_adv_equals(&adv_config, &contents),
             Some(_) => {
                 // we generated an encrypted adv, but didn't include the credential
-                assert_eq!(V0AdvContents::NoMatchingCredentials, contents);
+                assert_eq!(V0AdvertisementContents::NoMatchingCredentials, contents);
             }
         }
     }
 }
 
-fn assert_adv_equals<'m>(
+/// Short-hand for asserting that the contents of two V0 advertisements
+/// are the same for tests where we only ever have 0-1 broadcasting
+/// identities in play.
+fn assert_adv_equals<M: MatchedCredential + AsRef<EmptyMatchedCredential>>(
     adv_config: &AdvConfig,
-    adv: &V0AdvContents<'m, SimpleMatchedCredential<'m, [u8; 32]>>,
+    adv: &V0AdvertisementContents<M>,
 ) {
     match adv_config.identity {
         None => match adv {
-            V0AdvContents::Plaintext(p) => {
+            V0AdvertisementContents::Plaintext(p) => {
                 let mut action_bits = ActionBits::default();
                 action_bits.set_action(ContextSyncSeqNum::try_from(3).unwrap());
                 let de = ActionsDataElement::from(action_bits);
@@ -135,7 +157,7 @@
             _ => panic!("should be a plaintext adv"),
         },
         Some(_) => match adv {
-            V0AdvContents::Decrypted(wmc) => {
+            V0AdvertisementContents::Decrypted(wmc) => {
                 assert!(adv_config.plaintext_mode.is_none());
 
                 // different generic type param, so can't re-use the DE from above
@@ -152,7 +174,7 @@
                     wmc.contents().identity_type()
                 );
                 assert_eq!(
-                    &adv_config.identity.unwrap().legacy_metadata_key,
+                    adv_config.identity.unwrap().legacy_metadata_key,
                     wmc.contents().metadata_key()
                 );
             }
@@ -161,16 +183,19 @@
     }
 }
 
-fn deser_v0<'s, C, S, P>(
-    cred_source: &'s S,
-    adv: &[u8],
-) -> V0AdvContents<'s, SimpleMatchedCredential<'s, [u8; 32]>>
+fn deser_v0<'adv, B, P>(
+    arena: impl BorrowMut<DeserializationArena<'adv>>,
+    adv: &'adv [u8],
+    cred_book: &'adv B,
+) -> V0AdvertisementContents<B::Matched>
 where
-    C: V0Credential<Matched<'s> = SimpleMatchedCredential<'s, [u8; 32]>> + 's,
-    S: CredentialSource<C>,
+    B: CredentialBook<'adv>,
     P: CryptoProvider,
 {
-    deserialize_v0_advertisement::<C, S, P>(adv, cred_source).unwrap()
+    deserialize_advertisement::<_, P>(arena, adv, cred_book)
+        .expect("Should be a valid advertisement")
+        .into_v0()
+        .expect("Should be V0")
 }
 
 /// Populate an advertisement with a randomly chosen identity and a DE
@@ -179,38 +204,27 @@
     identities: &'a Vec<TestIdentity<CryptoProviderImpl>>,
 ) -> (ArrayView<u8, { BLE_ADV_SVC_CONTENT_LEN }>, AdvConfig<'a>) {
     let identity = identities.choose(&mut rng).unwrap();
-    match rng.gen_range(0_u8..=2) {
-        0 => {
-            let mut adv_builder = AdvBuilder::new(NoIdentity::default());
-            add_de(&mut adv_builder);
+    if rng.gen_bool(0.5) {
+        let mut adv_builder = AdvBuilder::new(PublicIdentity);
+        add_de(&mut adv_builder);
 
-            (
-                adv_builder.into_advertisement().unwrap(),
-                AdvConfig::new(None, Some(PlaintextIdentityMode::None)),
-            )
-        }
-        1 => {
-            let mut adv_builder = AdvBuilder::new(PublicIdentity::default());
-            add_de(&mut adv_builder);
+        (
+            adv_builder.into_advertisement().unwrap(),
+            AdvConfig::new(None, Some(PlaintextIdentityMode::Public)),
+        )
+    } else {
+        let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(
+            identity.key_seed,
+            identity.legacy_metadata_key,
+        );
+        let mut adv_builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
+            identity.identity_type,
+            LegacySalt::from(rng.gen::<[u8; 2]>()),
+            &broadcast_cm,
+        ));
+        add_de(&mut adv_builder);
 
-            (
-                adv_builder.into_advertisement().unwrap(),
-                AdvConfig::new(None, Some(PlaintextIdentityMode::Public)),
-            )
-        }
-        2 => {
-            let mut adv_builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
-                identity.identity_type,
-                LegacySalt::from(rng.gen::<[u8; 2]>()),
-                identity.legacy_metadata_key,
-                LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(&identity.hkdf().legacy_ldt_key()),
-            ));
-            add_de(&mut adv_builder);
-
-            (adv_builder.into_advertisement().unwrap(), AdvConfig::new(Some(identity), None))
-        }
-
-        _ => unreachable!(),
+        (adv_builder.into_advertisement().unwrap(), AdvConfig::new(Some(identity), None))
     }
 }
 
@@ -229,7 +243,7 @@
 struct TestIdentity<C: CryptoProvider> {
     identity_type: EncryptedIdentityDataElementType,
     key_seed: [u8; 32],
-    legacy_metadata_key: [u8; 14],
+    legacy_metadata_key: ShortMetadataKey,
     _marker: PhantomData<C>,
 }
 
@@ -242,20 +256,17 @@
                 .choose(rng)
                 .unwrap(),
             key_seed: rng.gen(),
-            legacy_metadata_key: rng.gen(),
+            legacy_metadata_key: ShortMetadataKey(rng.gen()),
             _marker: PhantomData,
         }
     }
 
-    /// Returns a credential using crypto material from this identity
-    fn credential(&self) -> SimpleV0Credential<MinimumFootprintV0CryptoMaterial, [u8; 32]> {
+    /// Returns a discovery-credential using crypto material from this identity
+    fn discovery_credential(&self) -> V0DiscoveryCredential {
         let hkdf = self.hkdf();
-        SimpleV0Credential::new(
-            MinimumFootprintV0CryptoMaterial::new(
-                self.key_seed,
-                hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&self.legacy_metadata_key),
-            ),
+        V0DiscoveryCredential::new(
             self.key_seed,
+            hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&self.legacy_metadata_key.0),
         )
     }
 
diff --git a/nearby/presence/np_adv/src/deser_v1_tests.rs b/nearby/presence/np_adv/src/deser_v1_tests.rs
index b59b18b..80f646e 100644
--- a/nearby/presence/np_adv/src/deser_v1_tests.rs
+++ b/nearby/presence/np_adv/src/deser_v1_tests.rs
@@ -19,79 +19,133 @@
 
 use crate::{
     credential::{
-        simple::{SimpleMatchedCredential, SimpleV1Credential},
-        source::SliceCredentialSource,
-        v1::MinimumFootprintV1CryptoMaterial,
+        book::{CredentialBook, CredentialBookBuilder},
+        v1::{
+            SignedBroadcastCryptoMaterial, SimpleSignedBroadcastCryptoMaterial,
+            V1DiscoveryCredential,
+        },
+        EmptyMatchedCredential, MatchableCredential, MatchedCredential,
     },
     de_type::EncryptedIdentityDataElementType,
-    deserialize_v1_advertisement,
+    deserialization_arena,
+    deserialization_arena::DeserializationArena,
+    deserialize_advertisement,
     extended::{
         data_elements::GenericDataElement,
         deserialize::VerificationMode,
         serialize::{
-            AdvBuilder, EncodedAdvertisement, MicEncrypted, SectionBuilder, SectionIdentity,
-            SignedEncrypted,
+            AdvBuilder, AdvertisementType, EncodedAdvertisement, MicEncryptedSectionEncoder,
+            PublicSectionEncoder, SectionBuilder, SectionEncoder, SignedEncryptedSectionEncoder,
         },
     },
-    AdvDeserializationError, AdvDeserializationErrorDetailsHazmat, CredentialSource,
-    PlaintextIdentityMode, PublicIdentity, Section, V1AdvContents, V1Credential,
-    V1DeserializedSection,
+    AdvDeserializationError, AdvDeserializationErrorDetailsHazmat, HasIdentityMatch, MetadataKey,
+    PlaintextIdentityMode, Section, V1AdvertisementContents, V1DeserializedSection,
 };
 use crypto_provider::{CryptoProvider, CryptoRng};
 use std::{collections, prelude::rust_2021::*, vec};
 use strum::IntoEnumIterator as _;
 
+use crate::extended::NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT;
 use crypto_provider_default::CryptoProviderImpl;
 
 #[test]
-fn v1_all_identities_resolvable_mix_plaintext_ciphertext() {
+fn v1_plaintext() {
     let mut rng = StdRng::from_entropy();
     for _ in 0..100 {
         let identities = (0..100)
             .map(|_| TestIdentity::<CryptoProviderImpl>::random(&mut rng))
             .collect::<Vec<_>>();
 
-        let mut adv_builder = AdvBuilder::new();
-        let section_configs = fill_adv(&mut rng, &identities, &mut adv_builder);
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+        let section_configs: Vec<SectionConfig<CryptoProviderImpl>> =
+            fill_plaintext_adv(&mut rng, &mut adv_builder);
         let adv = adv_builder.into_advertisement();
-        let creds = identities.iter().map(|i| i.credential()).collect::<Vec<_>>();
-        let cred_source = SliceCredentialSource::new(&creds);
-        // check if the section header would be 0 or if the section is empty
-        match section_configs.is_empty() {
-            true => {
-                let v1_error = deser_v1_error::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                assert_eq!(
-                    v1_error,
-                    AdvDeserializationError::ParseError {
-                        details_hazmat:
-                            AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
-                    }
-                ); //assert a adv deserialization error
-            }
-            false => {
-                let v1_contents = deser_v1::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                assert_eq!(0, v1_contents.invalid_sections_count());
-                assert_eq!(section_configs.len(), v1_contents.sections.len());
-                for (section_config, section) in
-                    section_configs.iter().zip(v1_contents.sections.iter())
-                {
-                    assert_section_equals(section_config, section);
+        let creds = identities
+            .iter()
+            .map(|i| MatchableCredential {
+                discovery_credential: i.discovery_credential(),
+                match_data: EmptyMatchedCredential,
+            })
+            .collect::<Vec<_>>();
+
+        let mut arena = deserialization_arena!();
+        // check if the section is empty or there is more than one public section
+        let cred_book =
+            CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&[], &creds);
+        if section_configs.is_empty() {
+            let v1_error = deser_v1_error::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(
+                v1_error,
+                AdvDeserializationError::ParseError {
+                    details_hazmat:
+                        AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
                 }
+            ); //assert a adv deserialization error
+        } else {
+            let v1_contents = deser_v1::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(0, v1_contents.invalid_sections_count());
+            assert_eq!(section_configs.len(), v1_contents.sections.len());
+            for (section_config, section) in section_configs.iter().zip(v1_contents.sections.iter())
+            {
+                assert_section_equals(section_config, section);
             }
         }
     }
 }
 
 #[test]
-fn v1_only_non_matching_identities_available_mix_plaintext_ciphertext() {
+fn v1_all_identities_resolvable_ciphertext() {
     let mut rng = StdRng::from_entropy();
     for _ in 0..100 {
         let identities = (0..100)
             .map(|_| TestIdentity::<CryptoProviderImpl>::random(&mut rng))
             .collect::<Vec<_>>();
 
-        let mut adv_builder = AdvBuilder::new();
-        let section_configs = fill_adv(&mut rng, &identities, &mut adv_builder);
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+        let section_configs = fill_encrypted_adv(&mut rng, &identities, &mut adv_builder);
+        let adv = adv_builder.into_advertisement();
+        let creds = identities
+            .iter()
+            .map(|i| MatchableCredential {
+                discovery_credential: i.discovery_credential(),
+                match_data: EmptyMatchedCredential,
+            })
+            .collect::<Vec<_>>();
+        let mut arena = deserialization_arena!();
+        // check if the section header would be 0 or if the section is empty
+        let cred_book =
+            CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&[], &creds);
+        if section_configs.is_empty() {
+            let v1_error = deser_v1_error::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(
+                v1_error,
+                AdvDeserializationError::ParseError {
+                    details_hazmat:
+                        AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
+                }
+            ); //assert a adv deserialization error
+        } else {
+            let v1_contents = deser_v1::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(0, v1_contents.invalid_sections_count());
+            assert_eq!(section_configs.len(), v1_contents.sections.len());
+            for (section_config, section) in section_configs.iter().zip(v1_contents.sections.iter())
+            {
+                assert_section_equals(section_config, section);
+            }
+        }
+    }
+}
+
+#[test]
+fn v1_only_non_matching_identities_available_ciphertext() {
+    let mut rng = StdRng::from_entropy();
+    for _ in 0..100 {
+        let identities = (0..100)
+            .map(|_| TestIdentity::<CryptoProviderImpl>::random(&mut rng))
+            .collect::<Vec<_>>();
+
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+        let section_configs = fill_encrypted_adv(&mut rng, &identities, &mut adv_builder);
         let adv = adv_builder.into_advertisement();
         let creds = identities
             .iter()
@@ -101,104 +155,103 @@
                     .iter()
                     .any(|sc| sc.identity.map(|sci| sci.key_seed == i.key_seed).unwrap_or(false))
             })
-            .map(|i| i.credential())
+            .map(|i| MatchableCredential {
+                discovery_credential: i.discovery_credential(),
+                match_data: EmptyMatchedCredential,
+            })
             .collect::<Vec<_>>();
-        let cred_source = SliceCredentialSource::new(&creds);
+
+        let mut arena = deserialization_arena!();
         // check if the section header would be 0
-        match section_configs.is_empty() {
-            true => {
-                let v1_error = deser_v1_error::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                assert_eq!(
-                    v1_error,
-                    AdvDeserializationError::ParseError {
-                        details_hazmat:
-                            AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
-                    }
-                ); //assert a adv deserialization error
-            }
-            false => {
-                let v1_contents = deser_v1::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                // all encrypted identity sections are invalid
-                let encrypted_section_count =
-                    section_configs.iter().filter(|sc| sc.identity.is_some()).count();
-                assert_eq!(encrypted_section_count, v1_contents.invalid_sections_count());
-                assert_eq!(
-                    section_configs.len() - encrypted_section_count,
-                    v1_contents.sections.len()
-                );
-                for (section_config, section) in section_configs
-                    .iter()
-                    // skip encrypted sections
-                    .filter(|sc| sc.identity.is_none())
-                    .zip(v1_contents.sections.iter())
-                {
-                    assert_section_equals(section_config, section);
+        let cred_book =
+            CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&[], &creds);
+        if section_configs.is_empty() {
+            let v1_error = deser_v1_error::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(
+                v1_error,
+                AdvDeserializationError::ParseError {
+                    details_hazmat:
+                        AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
                 }
+            ); //assert a adv deserialization error
+        } else {
+            let v1_contents = deser_v1::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            // all encrypted identity sections are invalid
+            let encrypted_section_count =
+                section_configs.iter().filter(|sc| sc.identity.is_some()).count();
+            assert_eq!(encrypted_section_count, v1_contents.invalid_sections_count());
+            assert_eq!(section_configs.len() - encrypted_section_count, v1_contents.sections.len());
+            for (section_config, section) in section_configs
+                .iter()
+                // skip encrypted sections
+                .filter(|sc| sc.identity.is_none())
+                .zip(v1_contents.sections.iter())
+            {
+                assert_section_equals(section_config, section);
             }
         }
     }
 }
 
 #[test]
-fn v1_no_creds_available_mix_plaintext_ciphertext() {
+fn v1_no_creds_available_ciphertext() {
     let mut rng = StdRng::from_entropy();
     for _ in 0..100 {
         let identities = (0..100).map(|_| TestIdentity::random(&mut rng)).collect::<Vec<_>>();
 
-        let mut adv_builder = AdvBuilder::new();
-        let section_configs =
-            fill_adv::<StdRng, CryptoProviderImpl>(&mut rng, &identities, &mut adv_builder);
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+        let section_configs = fill_encrypted_adv::<StdRng, CryptoProviderImpl>(
+            &mut rng,
+            &identities,
+            &mut adv_builder,
+        );
         let adv = adv_builder.into_advertisement();
-        let cred_source: SliceCredentialSource<
-            '_,
-            SimpleV1Credential<MinimumFootprintV1CryptoMaterial, [u8; 32]>,
-        > = SliceCredentialSource::new(&[]);
+        let mut arena = deserialization_arena!();
         // check if the section header would be 0
-        match section_configs.is_empty() {
-            true => {
-                let v1_error = deser_v1_error::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                assert_eq!(
-                    v1_error,
-                    AdvDeserializationError::ParseError {
-                        details_hazmat:
-                            AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
-                    }
-                ); //assert a adv deserialization error
-            }
-            false => {
-                let v1_contents = deser_v1::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                // all encrypted identity sections are invalid
-                let encrypted_section_count =
-                    section_configs.iter().filter(|sc| sc.identity.is_some()).count();
-                assert_eq!(encrypted_section_count, v1_contents.invalid_sections_count());
-                assert_eq!(
-                    section_configs.len() - encrypted_section_count,
-                    v1_contents.sections.len()
-                );
-
-                for (section_config, section) in section_configs
-                    .iter()
-                    // skip encrypted sections
-                    .filter(|sc| sc.identity.is_none())
-                    .zip(v1_contents.sections.iter())
-                {
-                    assert_section_equals(section_config, section);
+        let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+            0,
+            0,
+            CryptoProviderImpl,
+        >(&[], &[]);
+        if section_configs.is_empty() {
+            let v1_error = deser_v1_error::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(
+                v1_error,
+                AdvDeserializationError::ParseError {
+                    details_hazmat:
+                        AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
                 }
+            ); //assert a adv deserialization error
+        } else {
+            let v1_contents = deser_v1::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            // all encrypted identity sections are invalid
+            let encrypted_section_count =
+                section_configs.iter().filter(|sc| sc.identity.is_some()).count();
+            assert_eq!(encrypted_section_count, v1_contents.invalid_sections_count());
+            assert_eq!(section_configs.len() - encrypted_section_count, v1_contents.sections.len());
+
+            for (section_config, section) in section_configs
+                .iter()
+                // skip encrypted sections
+                .filter(|sc| sc.identity.is_none())
+                .zip(v1_contents.sections.iter())
+            {
+                assert_section_equals(section_config, section);
             }
         }
     }
 }
 
 #[test]
-fn v1_only_some_matching_identities_available_mix_plaintext_ciphertext() {
+fn v1_only_some_matching_identities_available_ciphertext() {
     let mut rng = StdRng::from_entropy();
     for _ in 0..100 {
         let identities = (0..100)
             .map(|_| TestIdentity::<CryptoProviderImpl>::random(&mut rng))
             .collect::<Vec<_>>();
 
-        let mut adv_builder = AdvBuilder::new();
-        let section_configs = fill_adv(&mut rng, &identities, &mut adv_builder);
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+        let section_configs = fill_encrypted_adv(&mut rng, &identities, &mut adv_builder);
         let adv = adv_builder.into_advertisement();
         // identities used in sections, which may be used in multiple sections too
         let identities_to_remove: collections::HashSet<_> = identities
@@ -217,59 +270,59 @@
         let creds = identities
             .iter()
             .filter(|i| !identities_to_remove.contains(&i.key_seed))
-            .map(|i| i.credential())
+            .map(|i| MatchableCredential {
+                discovery_credential: i.discovery_credential(),
+                match_data: EmptyMatchedCredential,
+            })
             .collect::<Vec<_>>();
-        let cred_source = SliceCredentialSource::new(&creds);
+
+        let mut arena = deserialization_arena!();
+
+        let cred_book =
+            CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&[], &creds);
+
         // check if the section header would be 0
-        match section_configs.is_empty() {
-            true => {
-                let v1_error = deser_v1_error::<_, _, CryptoProviderImpl>(&cred_source, adv);
-                assert_eq!(
-                    v1_error,
-                    AdvDeserializationError::ParseError {
-                        details_hazmat:
-                            AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
-                    }
-                ); //assert a adv deserialization error
-            }
-            false => {
-                let v1_contents = deser_v1::<_, _, CryptoProviderImpl>(&cred_source, adv);
-
-                let affected_sections: Vec<_> = section_configs
-                    .iter()
-                    .filter(|sc| {
-                        sc.identity
-                            .map(|sci| identities_to_remove.iter().any(|ks| &sci.key_seed == ks))
-                            .unwrap_or(false)
-                    })
-                    .collect();
-
-                assert_eq!(affected_sections.len(), v1_contents.invalid_sections_count());
-                assert_eq!(
-                    section_configs.len() - affected_sections.len(),
-                    v1_contents.sections.len()
-                );
-
-                for (section_config, section) in section_configs
-                    .iter()
-                    // skip sections w/ removed identities
-                    .filter(|sc| {
-                        sc.identity
-                            .map(|i| !identities_to_remove.contains(&i.key_seed))
-                            .unwrap_or(true)
-                    })
-                    .zip(v1_contents.sections.iter())
-                {
-                    assert_section_equals(section_config, section);
+        if section_configs.is_empty() {
+            let v1_error = deser_v1_error::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+            assert_eq!(
+                v1_error,
+                AdvDeserializationError::ParseError {
+                    details_hazmat:
+                        AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
                 }
+            ); //assert a adv deserialization error
+        } else {
+            let v1_contents = deser_v1::<_, CryptoProviderImpl>(&mut arena, &adv, &cred_book);
+
+            let affected_sections: Vec<_> = section_configs
+                .iter()
+                .filter(|sc| {
+                    sc.identity
+                        .map(|sci| identities_to_remove.iter().any(|ks| &sci.key_seed == ks))
+                        .unwrap_or(false)
+                })
+                .collect();
+
+            assert_eq!(affected_sections.len(), v1_contents.invalid_sections_count());
+            assert_eq!(section_configs.len() - affected_sections.len(), v1_contents.sections.len());
+
+            for (section_config, section) in section_configs
+                .iter()
+                // skip sections w/ removed identities
+                .filter(|sc| {
+                    sc.identity.map(|i| !identities_to_remove.contains(&i.key_seed)).unwrap_or(true)
+                })
+                .zip(v1_contents.sections.iter())
+            {
+                assert_section_equals(section_config, section);
             }
         }
     }
 }
 
-fn assert_section_equals<'m, C: CryptoProvider>(
+fn assert_section_equals<M: MatchedCredential, C: CryptoProvider>(
     section_config: &SectionConfig<C>,
-    section: &V1DeserializedSection<'m, SimpleMatchedCredential<'m, [u8; 32]>>,
+    section: &V1DeserializedSection<M>,
 ) {
     match section_config.identity {
         None => match section {
@@ -279,7 +332,7 @@
                 assert_eq!(section_config.plaintext_mode.unwrap(), p.identity());
                 assert_eq!(
                     section_config.data_elements,
-                    p.data_elements().map(|de| de.into()).collect::<Vec<_>>()
+                    p.iter_data_elements().map(|de| (&de.unwrap()).into()).collect::<Vec<_>>()
                 )
             }
             V1DeserializedSection::Decrypted(_) => panic!("no id, but decrypted section"),
@@ -291,14 +344,17 @@
 
                 assert_eq!(
                     section_config.data_elements,
-                    wmc.contents().data_elements().map(|de| de.into()).collect::<Vec<_>>()
+                    wmc.contents()
+                        .iter_data_elements()
+                        .map(|de| (&de.unwrap()).into())
+                        .collect::<Vec<GenericDataElement>>()
                 );
                 assert_eq!(
                     section_config.identity.unwrap().identity_type,
                     wmc.contents().identity_type()
                 );
                 assert_eq!(
-                    &section_config.identity.unwrap().extended_metadata_key,
+                    section_config.identity.unwrap().extended_metadata_key,
                     wmc.contents().metadata_key()
                 );
                 assert_eq!(
@@ -310,35 +366,66 @@
     }
 }
 
-fn deser_v1_error<'s, C, S, P>(
-    cred_source: &'s S,
-    adv: EncodedAdvertisement,
+fn deser_v1_error<'a, B, P>(
+    arena: &'a mut DeserializationArena<'a>,
+    adv: &'a EncodedAdvertisement,
+    cred_book: &'a B,
 ) -> AdvDeserializationError
 where
-    C: V1Credential<Matched<'s> = SimpleMatchedCredential<'s, [u8; 32]>> + 's,
-    S: CredentialSource<C>,
+    B: CredentialBook<'a>,
     P: CryptoProvider,
 {
-    let v1_contents = match deserialize_v1_advertisement::<C, S, P>(adv.as_slice(), cred_source) {
+    let v1_contents = match deserialize_advertisement::<_, P>(arena, adv.as_slice(), cred_book) {
         Err(e) => e,
         _ => panic!("Expecting an error!"),
     };
     v1_contents
 }
 
-fn deser_v1<'s, C, S, P>(
-    cred_source: &'s S,
-    adv: EncodedAdvertisement,
-) -> V1AdvContents<'s, SimpleMatchedCredential<'s, [u8; 32]>>
+fn deser_v1<'adv, B, P>(
+    arena: &'adv mut DeserializationArena<'adv>,
+    adv: &'adv EncodedAdvertisement,
+    cred_book: &'adv B,
+) -> V1AdvertisementContents<'adv, B::Matched>
 where
-    C: V1Credential<Matched<'s> = SimpleMatchedCredential<'s, [u8; 32]>> + 's,
-    S: CredentialSource<C>,
+    B: CredentialBook<'adv>,
     P: CryptoProvider,
 {
-    deserialize_v1_advertisement::<C, S, P>(adv.as_slice(), cred_source).unwrap()
+    deserialize_advertisement::<_, P>(arena, adv.as_slice(), cred_book)
+        .expect("Should be a valid advertisement")
+        .into_v1()
+        .expect("Should be V1")
 }
+
 /// Populate a random number of sections with randomly chosen identities and random DEs
-fn fill_adv<'a, R: rand::Rng, C: CryptoProvider>(
+fn fill_plaintext_adv<'a, R: rand::Rng, C: CryptoProvider>(
+    mut rng: &mut R,
+    adv_builder: &mut AdvBuilder,
+) -> Vec<SectionConfig<'a, C>> {
+    let mut expected = Vec::new();
+    // build sections
+    for _ in 0..rng.gen_range(0..=NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT) {
+        let res = adv_builder.section_builder(PublicSectionEncoder::default()).map(|s| {
+            SectionConfig::new(
+                None,
+                Some(PlaintextIdentityMode::Public),
+                None,
+                add_des(s, &mut rng),
+            )
+        });
+        match res {
+            Ok(tuple) => expected.push(tuple),
+            Err(_) => {
+                // couldn't fit that section; maybe another smaller section will fit
+                continue;
+            }
+        }
+    }
+    expected
+}
+
+/// Populate a random number of sections with randomly chosen identities and random DEs
+fn fill_encrypted_adv<'a, R: rand::Rng, C: CryptoProvider>(
     mut rng: &mut R,
     identities: &'a Vec<TestIdentity<C>>,
     adv_builder: &mut AdvBuilder,
@@ -349,21 +436,15 @@
     for _ in 0..rng.gen_range(0..=6) {
         let chosen_index = rng.gen_range(0..identities.len());
         let identity = &identities[chosen_index];
-        let res = match rng.gen_range(1_u8..=3) {
-            1 => adv_builder.section_builder(PublicIdentity::default()).map(|s| {
-                SectionConfig::new(
-                    None,
-                    Some(PlaintextIdentityMode::Public),
-                    None,
-                    add_des(s, &mut rng),
-                )
-            }),
-            2 => adv_builder
-                .section_builder(MicEncrypted::new_random_salt(
+
+        let broadcast_cm = identity.broadcast_credential();
+
+        let res = if rng.gen_bool(0.5) {
+            adv_builder
+                .section_builder(MicEncryptedSectionEncoder::<C>::new_random_salt(
                     &mut salt_rng,
                     identity.identity_type,
-                    &identity.extended_metadata_key,
-                    &identity.hkdf(),
+                    &broadcast_cm,
                 ))
                 .map(|s| {
                     SectionConfig::new(
@@ -372,14 +453,13 @@
                         Some(VerificationMode::Mic),
                         add_des(s, &mut rng),
                     )
-                }),
-            3 => adv_builder
-                .section_builder(SignedEncrypted::new_random_salt(
+                })
+        } else {
+            adv_builder
+                .section_builder(SignedEncryptedSectionEncoder::<C>::new_random_salt(
                     &mut salt_rng,
                     identity.identity_type,
-                    &identity.extended_metadata_key,
-                    &identity.key_pair,
-                    &identity.hkdf(),
+                    &broadcast_cm,
                 ))
                 .map(|s| {
                     SectionConfig::new(
@@ -388,8 +468,7 @@
                         Some(VerificationMode::Signature),
                         add_des(s, &mut rng),
                     )
-                }),
-            _ => unreachable!(),
+                })
         };
         match res {
             Ok(tuple) => expected.push(tuple),
@@ -401,10 +480,11 @@
     }
     expected
 }
+
 struct TestIdentity<C: CryptoProvider> {
     identity_type: EncryptedIdentityDataElementType,
     key_seed: [u8; 32],
-    extended_metadata_key: [u8; 16],
+    extended_metadata_key: MetadataKey,
     key_pair: np_ed25519::KeyPair<C>,
     marker: PhantomData<C>,
 }
@@ -417,32 +497,26 @@
                 .choose(rng)
                 .unwrap(),
             key_seed: rng.gen(),
-            extended_metadata_key: rng.gen(),
+            extended_metadata_key: MetadataKey(rng.gen()),
             key_pair: np_ed25519::KeyPair::<C>::generate(),
             marker: PhantomData,
         }
     }
-    /// Returns a credential using crypto material from this identity
-    fn credential(&self) -> SimpleV1Credential<MinimumFootprintV1CryptoMaterial, [u8; 32]> {
-        let hkdf = self.hkdf();
-        SimpleV1Credential::new(
-            MinimumFootprintV1CryptoMaterial::new(
-                self.key_seed,
-                hkdf.extended_unsigned_metadata_key_hmac_key()
-                    .calculate_hmac(&self.extended_metadata_key),
-                hkdf.extended_signed_metadata_key_hmac_key()
-                    .calculate_hmac(&self.extended_metadata_key),
-                self.key_pair.public(),
-            ),
+    /// Returns a (simple, signed) broadcast credential using crypto material from this identity
+    fn broadcast_credential(&self) -> SimpleSignedBroadcastCryptoMaterial {
+        SimpleSignedBroadcastCryptoMaterial::new(
             self.key_seed,
+            self.extended_metadata_key,
+            self.key_pair.private_key(),
         )
     }
-    fn hkdf(&self) -> np_hkdf::NpKeySeedHkdf<C> {
-        np_hkdf::NpKeySeedHkdf::<C>::new(&self.key_seed)
+    /// Returns a discovery credential using crypto material from this identity
+    fn discovery_credential(&self) -> V1DiscoveryCredential {
+        self.broadcast_credential().derive_v1_discovery_credential::<C>()
     }
 }
 /// Add several DEs with random types and contents
-fn add_des<I: SectionIdentity, R: rand::Rng>(
+fn add_des<I: SectionEncoder, R: rand::Rng>(
     mut sb: SectionBuilder<I>,
     rng: &mut R,
 ) -> Vec<GenericDataElement> {
diff --git a/nearby/presence/np_adv/src/deserialization_arena.rs b/nearby/presence/np_adv/src/deserialization_arena.rs
new file mode 100644
index 0000000..6b5f651
--- /dev/null
+++ b/nearby/presence/np_adv/src/deserialization_arena.rs
@@ -0,0 +1,102 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Types for creating arenas used in deserialization of np_adv. This implementation is purpose-made
+//! for deserializing in `np_adv` and is not intended for general use as an arena.
+
+use crate::extended::BLE_ADV_SVC_CONTENT_LEN;
+
+/// Create a [`DeserializationArena`] suitable for use with deserializing an advertisement.
+#[macro_export]
+macro_rules! deserialization_arena {
+    // Trick borrowed from `pin!`: In an argument to a braced constructor, if we take a reference to
+    // a temporary value, that value will be upgraded to live for the scope of the enclosing block,
+    // avoiding another `let` binding for the buffer which is normally required to keep the buffer
+    // on the stack.
+    // Reference: https://doc.rust-lang.org/reference/destructors.html#temporary-lifetime-extension
+    () => {
+        $crate::deserialization_arena::DeserializationArena {
+            slice: &mut $crate::deserialization_arena::DeserializationArena::buffer(),
+        }
+    };
+}
+
+/// A simple allocator that simply keeps splitting the given slice on allocation. Use the
+/// [`deserialization_arena!`][crate::deserialization_arena!] macro to create an instance.
+pub struct DeserializationArena<'a> {
+    #[doc(hidden)] // Exposed for use by `deserialization_arena!` only.
+    pub slice: &'a mut [u8],
+}
+
+impl<'a> DeserializationArena<'a> {
+    #[doc(hidden)] // Exposed for use by `deserialization_arena!` only.
+    pub fn buffer() -> [u8; BLE_ADV_SVC_CONTENT_LEN] {
+        [0; BLE_ADV_SVC_CONTENT_LEN]
+    }
+
+    /// Allocates `len` bytes from the slice given upon construction. In the expected use case, the
+    /// allocated slice should be written to with actual data, overwriting what's contained in
+    /// there. While reading from the allocated slice without first writing to it is safe in the
+    /// Rust memory-safety sense, the returned slice contains "garbage" data from the slice given
+    /// during construction.
+    ///
+    /// Returns an error with [`ArenaOutOfSpace`] if the remaining slice is not long enough to
+    /// allocate `len` bytes.
+    pub fn allocate(&mut self, len: u8) -> Result<&'a mut [u8], ArenaOutOfSpace> {
+        if usize::from(len) > self.slice.len() {
+            return Err(ArenaOutOfSpace);
+        }
+        let (allocated, remaining) = core::mem::take(&mut self.slice).split_at_mut(len.into());
+        self.slice = remaining;
+        // Note: the returned data is logically garbage, but in practice it's all zeroes assuming
+        // `deserialization_arena!` was used to create this allocator.
+        Ok(allocated)
+    }
+}
+
+/// Error indicating that the arena has ran out of space, and deserialization cannot proceed. This
+/// should never happen if the arena is created with [`crate::deserialization_arena!`], since the
+/// total size of decrypted sections should be less than the size of the incoming BLE advertisement.
+#[derive(Debug, PartialEq, Eq)]
+pub struct ArenaOutOfSpace;
+
+#[cfg(test)]
+mod test {
+    use crate::{deserialization_arena::ArenaOutOfSpace, extended::BLE_ADV_SVC_CONTENT_LEN};
+
+    #[test]
+    fn test_creation() {
+        assert_eq!(BLE_ADV_SVC_CONTENT_LEN, deserialization_arena!().slice.len());
+    }
+
+    #[test]
+    fn test_allocation() {
+        let mut arena = deserialization_arena!();
+        assert_eq!(Ok(&mut [0_u8; 100][..]), arena.allocate(100));
+        assert_eq!(BLE_ADV_SVC_CONTENT_LEN - 100, arena.slice.len());
+    }
+
+    #[test]
+    fn test_allocation_overflow() {
+        let mut arena = deserialization_arena!();
+        assert_eq!(Err(ArenaOutOfSpace), arena.allocate(u8::MAX));
+    }
+
+    #[test]
+    fn test_allocation_twice_overflow() {
+        let mut arena = deserialization_arena!();
+        assert_eq!(Ok(&mut [0_u8; 128][..]), arena.allocate(128));
+        assert_eq!(Err(ArenaOutOfSpace), arena.allocate(128));
+    }
+}
diff --git a/nearby/presence/np_adv/src/extended/data_elements/mod.rs b/nearby/presence/np_adv/src/extended/data_elements/mod.rs
index 2058be1..4e6e481 100644
--- a/nearby/presence/np_adv/src/extended/data_elements/mod.rs
+++ b/nearby/presence/np_adv/src/extended/data_elements/mod.rs
@@ -67,8 +67,8 @@
 }
 
 /// Convert a deserialized DE into one you can serialize
-impl<'a> From<DataElement<'a>> for GenericDataElement {
-    fn from(de: DataElement<'a>) -> Self {
+impl<'a> From<&'a DataElement<'a>> for GenericDataElement {
+    fn from(de: &'a DataElement<'a>) -> Self {
         Self::try_from(de.de_type(), de.contents())
             .expect("Deserialized DE must have a valid length")
     }
diff --git a/nearby/presence/np_adv/src/extended/data_elements/tests.rs b/nearby/presence/np_adv/src/extended/data_elements/tests.rs
index a8938b0..a0f9a2f 100644
--- a/nearby/presence/np_adv/src/extended/data_elements/tests.rs
+++ b/nearby/presence/np_adv/src/extended/data_elements/tests.rs
@@ -15,16 +15,16 @@
 extern crate std;
 
 use super::*;
+use crate::extended::serialize::AdvertisementType;
 use crate::{
-    extended::serialize::{section_tests::SectionBuilderExt, AdvBuilder},
+    extended::serialize::{section_tests::SectionBuilderExt, AdvBuilder, PublicSectionEncoder},
     shared_data::TxPower,
-    PublicIdentity,
 };
 
 #[test]
 fn serialize_tx_power_de() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder.add_de_res(|_| TxPower::try_from(3_i8).map(TxPowerDataElement::from)).unwrap();
 
@@ -41,8 +41,8 @@
 
 #[test]
 fn serialize_actions_de_empty() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder.add_de_res(|_| ActionsDataElement::try_from_actions(&[])).unwrap();
 
@@ -58,8 +58,8 @@
 
 #[test]
 fn serialize_actions_de_non_empty() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder
         .add_de_res(|_| ActionsDataElement::try_from_actions(&[1, 1, 2, 3, 5, 8]))
@@ -78,8 +78,8 @@
 
 #[test]
 fn serialize_context_sync_seq_num_de() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder
         .add_de_res(|_| ContextSyncSeqNum::try_from(3).map(ContextSyncSeqNumDataElement::from))
@@ -98,8 +98,8 @@
 
 #[test]
 fn serialize_connectivity_info_de_bluetooth() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder.add_de(|_| ConnectivityInfoDataElement::bluetooth([1; 4], [2; 6])).unwrap();
 
@@ -118,8 +118,8 @@
 
 #[test]
 fn serialize_connectivity_info_de_mdns() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder.add_de(|_| ConnectivityInfoDataElement::mdns([1; 4], 2)).unwrap();
 
@@ -138,8 +138,8 @@
 
 #[test]
 fn serialize_connectivity_info_de_wifi_direct() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder
         .add_de(|_| ConnectivityInfoDataElement::wifi_direct([1; 10], [2; 10], [3; 2], 4))
@@ -162,8 +162,8 @@
 
 #[test]
 fn serialize_connectivity_capabilities_de_wifi_direct() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     section_builder
         .add_de(|_| ConnectivityCapabilityDataElement::wifi_direct([1; 3], [2; 3]))
diff --git a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mic_decrypt_tests.rs b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mic_decrypt_tests.rs
index d112ead..dfab7ce 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mic_decrypt_tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mic_decrypt_tests.rs
@@ -15,20 +15,25 @@
 extern crate std;
 
 use super::*;
+use crate::deserialization_arena;
 use crate::extended::data_elements::TxPowerDataElement;
-use crate::extended::serialize::{SingleTypeDataElement, WriteDataElement};
+use crate::extended::deserialize::DataElementParseError;
+use crate::extended::serialize::{AdvertisementType, SingleTypeDataElement, WriteDataElement};
 use crate::shared_data::TxPower;
 use crate::{
-    credential::{simple::SimpleV1Credential, v1::MinimumFootprintV1CryptoMaterial},
+    credential::{v1::V1, SimpleBroadcastCryptoMaterial},
     de_type::EncryptedIdentityDataElementType,
     extended::{
         deserialize::{
-            encrypted_section::MicEncryptedSectionDeserializationError,
+            encrypted_section::{
+                EncryptedSectionContents, IdentityResolutionOrDeserializationError,
+                MicVerificationError,
+            },
             parse_sections,
-            test_stubs::{HkdfCryptoMaterial, IntermediateSectionExt},
-            CiphertextSection, OffsetDataElement, RawV1Salt,
+            test_stubs::IntermediateSectionExt,
+            CiphertextSection, DataElement, RawV1Salt,
         },
-        serialize::{AdvBuilder, CapacityLimitedVec, MicEncrypted},
+        serialize::{AdvBuilder, CapacityLimitedVec, MicEncryptedSectionEncoder},
     },
     parse_adv_header, AdvHeader, Section,
 };
@@ -39,20 +44,20 @@
 
 #[test]
 fn deserialize_mic_encrypted_correct_keys() {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let raw_salt = RawV1Salt([3; 16]);
     let section_salt = V1Salt::<CryptoProviderImpl>::from(raw_salt);
     let identity_type = EncryptedIdentityDataElementType::Private;
-    let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
 
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new_for_testing(
+        .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new(
             identity_type,
             V1Salt::from(*section_salt.as_array_ref()),
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -70,24 +75,38 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&adv_header, remaining).unwrap();
+    let sections = parse_sections(adv_header, remaining).unwrap();
     assert_eq!(1, sections.len());
 
     let section = sections.into_iter().next().unwrap();
     let enc_section = section.as_ciphertext().unwrap();
+
+    let contents = if let CiphertextSection::MicEncryptedIdentity(contents) = &enc_section {
+        contents
+    } else {
+        panic!("incorrect flavor");
+    };
+
     let keypair = np_ed25519::KeyPair::<CryptoProviderImpl>::generate();
 
     // deserializing to Section works
-    let credential = SimpleV1Credential::new(
-        MinimumFootprintV1CryptoMaterial::new::<CryptoProviderImpl>(
-            key_seed,
-            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            keypair.public(),
-        ),
-        (),
-    );
-    let section = enc_section.try_deserialize::<_, CryptoProviderImpl>(&credential).unwrap();
+    let discovery_credential =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, keypair.private_key())
+            .derive_v1_discovery_credential::<CryptoProviderImpl>();
+
+    let identity_resolution_material =
+        discovery_credential.unsigned_identity_resolution_material::<CryptoProviderImpl>();
+    let verification_material =
+        discovery_credential.unsigned_verification_material::<CryptoProviderImpl>();
+
+    let mut arena = deserialization_arena!();
+    let section = contents
+        .try_resolve_identity_and_deserialize::<CryptoProviderImpl>(
+            &mut arena,
+            &identity_resolution_material,
+            &verification_material,
+        )
+        .unwrap();
 
     assert_eq!(
         DecryptedSection::new(
@@ -97,37 +116,32 @@
             raw_salt,
             SectionContents {
                 section_header: 19 // encryption info de
-                    + 2 // de header
-                    + 16 // metadata key
-                    + 2 // de contents
-                    + 16, // mic hmac tag
+                        + 2 // de header
+                        + 16 // metadata key
+                        + 2 // de contents
+                        + 16, // mic hmac tag
                 // battery DE
-                de_data: ArrayView::try_from_slice(&[5]).unwrap(),
-                data_elements: vec![OffsetDataElement {
-                    offset: 2.into(),
-                    de_type: 0x05_u8.into(),
-                    start_of_contents: 0,
-                    contents_len: 1
-                }]
+                data_element_start_offset: 2,
+                de_region_excl_identity: &[txpower_de.de_header().serialize().as_slice(), &[5],]
+                    .concat(),
             }
         ),
         section
     );
+    let data_elements = section.collect_data_elements().unwrap();
+    assert_eq!(
+        data_elements,
+        &[DataElement { offset: 2.into(), de_type: 0x05_u8.into(), contents: &[5] }]
+    );
 
     assert_eq!(
-        vec![(DataElementOffset::from(2_usize), TxPowerDataElement::DE_TYPE, vec![5_u8])],
-        section
-            .data_elements()
+        vec![(DataElementOffset::from(2_u8), TxPowerDataElement::DE_TYPE, vec![5_u8])],
+        data_elements
+            .into_iter()
             .map(|de| (de.offset(), de.de_type(), de.contents().to_vec()))
             .collect::<Vec<_>>()
     );
 
-    let contents = if let CiphertextSection::MicEncryptedIdentity(contents) = &enc_section {
-        contents
-    } else {
-        panic!("incorrect flavor");
-    };
-
     let mut encryption_info_bytes = [0_u8; 19];
     encryption_info_bytes[0..2].copy_from_slice(&[0x91, 0x10]);
     encryption_info_bytes[2] = 0x00;
@@ -136,19 +150,21 @@
     let ciphertext_end = adv.as_slice().len() - 16;
     assert_eq!(
         &MicEncryptedSection {
-            section_header: 19 // encryption info de
+            contents: EncryptedSectionContents {
+                section_header: 19 // encryption info de
                     + 2 // de header
                     + 16 // metadata key
                     + 2 // de contents
                     + 16, // mic hmac tag
-            adv_header: &adv_header,
-            encryption_info: EncryptionInfo { bytes: encryption_info_bytes },
-            identity: EncryptedIdentityMetadata {
-                header_bytes: [0x90, 0x1],
-                offset: 1.into(),
-                identity_type: EncryptedIdentityDataElementType::Private,
+                adv_header,
+                encryption_info: EncryptionInfo { bytes: encryption_info_bytes },
+                identity: EncryptedIdentityMetadata {
+                    header_bytes: [0x90, 0x1],
+                    offset: 1.into(),
+                    identity_type: EncryptedIdentityDataElementType::Private,
+                },
+                all_ciphertext: &adv.as_slice()[1 + 1 + 19 + 2..ciphertext_end],
             },
-            all_ciphertext: &adv.as_slice()[1 + 1 + 19 + 2..ciphertext_end],
             mic: SectionMic { mic: adv.as_slice()[ciphertext_end..].try_into().unwrap() }
         },
         contents
@@ -156,30 +172,26 @@
 
     // plaintext is correct
     {
-        let crypto_material = MinimumFootprintV1CryptoMaterial::new::<CryptoProviderImpl>(
-            key_seed,
-            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            keypair.public(),
-        );
-        let identity_resolution_material =
-            crypto_material.unsigned_identity_resolution_material::<CryptoProviderImpl>();
-        let verification_material =
-            crypto_material.unsigned_verification_material::<CryptoProviderImpl>();
-
-        let decrypted = contents
-            .try_decrypt::<CryptoProviderImpl>(
-                &identity_resolution_material,
-                &verification_material,
+        let identity_resolution_contents =
+            contents.contents.compute_identity_resolution_contents::<CryptoProviderImpl>();
+        let identity_match = identity_resolution_contents
+            .try_match::<CryptoProviderImpl>(
+                &identity_resolution_material.into_raw_resolution_material(),
             )
             .unwrap();
+        let mut arena = deserialization_arena!();
+        let decrypted = contents
+            .contents
+            .decrypt_ciphertext::<CryptoProviderImpl>(&mut arena, identity_match)
+            .unwrap();
 
-        let mut expected = metadata_key.as_slice().to_vec();
+        let mut expected = Vec::new();
         // battery de
         expected.extend_from_slice(txpower_de.clone().de_header().serialize().as_slice());
         txpower_de.write_de_contents(&mut expected);
 
-        assert_eq!(&expected, decrypted.as_slice());
+        assert_eq!(metadata_key, decrypted.metadata_key_plaintext);
+        assert_eq!(&expected, decrypted.plaintext_contents);
     }
 }
 
@@ -187,7 +199,7 @@
 fn deserialize_mic_encrypted_incorrect_aes_key_error() {
     // bad aes key -> bad metadata key plaintext
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        MicEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        IdentityResolutionOrDeserializationError::IdentityMatchingError,
         Some([0xFF; 16].into()),
         None,
         None,
@@ -199,7 +211,7 @@
 fn deserialize_mic_encrypted_incorrect_metadata_key_hmac_key_error() {
     // bad metadata key hmac key -> bad calculated metadata key mac
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        MicEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        IdentityResolutionOrDeserializationError::IdentityMatchingError,
         None,
         Some([0xFF; 32].into()),
         None,
@@ -211,7 +223,7 @@
 fn deserialize_mic_encrypted_incorrect_mic_hmac_key_error() {
     // bad mic hmac key -> bad calculated mic
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        MicEncryptedSectionDeserializationError::MicMismatch,
+        MicVerificationError::MicMismatch.into(),
         None,
         None,
         Some([0xFF; 32].into()),
@@ -223,7 +235,7 @@
 fn deserialize_mic_encrypted_incorrect_expected_metadata_key_hmac_error() {
     // bad expected metadata key mac
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        MicEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        IdentityResolutionOrDeserializationError::IdentityMatchingError,
         None,
         None,
         None,
@@ -235,7 +247,9 @@
 fn deserialize_mic_encrypted_incorrect_salt_error() {
     // bad salt -> bad iv -> bad metadata key plaintext
     do_bad_deserialize_tampered(
-        MicEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            IdentityResolutionOrDeserializationError::IdentityMatchingError,
+        ),
         |_| {},
         |adv| adv[23..39].copy_from_slice(&[0xFF; 16]),
     );
@@ -245,7 +259,7 @@
 fn deserialize_mic_encrypted_de_that_wont_parse() {
     // add an extra byte to the section, leading it to try to parse a DE that doesn't exist
     do_bad_deserialize_tampered(
-        MicEncryptedSectionDeserializationError::DeParseError,
+        DeserializeError::DataElementParseError(DataElementParseError::UnexpectedDataAfterEnd),
         |sec| sec.try_push(0xFF).unwrap(),
         |_| {},
     );
@@ -255,7 +269,9 @@
 fn deserialize_mic_encrypted_tampered_mic_error() {
     // flip the a bit in the first MIC byte
     do_bad_deserialize_tampered(
-        MicEncryptedSectionDeserializationError::MicMismatch,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            MicVerificationError::MicMismatch.into(),
+        ),
         |_| {},
         |adv| {
             let mic_start = adv.len() - 16;
@@ -268,7 +284,9 @@
 fn deserialize_mic_encrypted_tampered_payload_error() {
     // flip the last payload bit
     do_bad_deserialize_tampered(
-        MicEncryptedSectionDeserializationError::MicMismatch,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            MicVerificationError::MicMismatch.into(),
+        ),
         |_| {},
         |adv| *adv.last_mut().unwrap() ^= 0x01,
     );
@@ -277,26 +295,27 @@
 /// Attempt a decryption that will fail when using the provided parameters for decryption only.
 /// `None` means use the correct value for that parameter.
 fn do_bad_deserialize_params<C: CryptoProvider>(
-    error: MicEncryptedSectionDeserializationError,
+    error: IdentityResolutionOrDeserializationError<MicVerificationError>,
     aes_key: Option<crypto_provider::aes::Aes128Key>,
     metadata_key_hmac_key: Option<np_hkdf::NpHmacSha256Key<C>>,
     mic_hmac_key: Option<np_hkdf::NpHmacSha256Key<C>>,
     expected_metadata_key_hmac: Option<[u8; 32]>,
 ) {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let section_salt: V1Salt<C> = [3; 16].into();
     let identity_type = EncryptedIdentityDataElementType::Private;
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::new(&key_seed);
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
 
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new_for_testing(
+        .section_builder(MicEncryptedSectionEncoder::<C>::new(
             identity_type,
             section_salt,
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -314,7 +333,7 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&v1_header, remaining).unwrap();
+    let sections = parse_sections(v1_header, remaining).unwrap();
     assert_eq!(1, sections.len());
 
     let section = sections.into_iter().next().unwrap();
@@ -331,7 +350,7 @@
             .unwrap_or_else(|| key_seed_hkdf.extended_unsigned_metadata_key_hmac_key())
             .as_bytes(),
         expected_metadata_key_hmac: expected_metadata_key_hmac.unwrap_or_else(|| {
-            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key)
+            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key.0)
         }),
     };
     let identity_resolution_material =
@@ -346,33 +365,42 @@
     assert_eq!(
         error,
         contents
-            .try_deserialize::<C>(&identity_resolution_material, &verification_material)
+            .try_resolve_identity_and_deserialize::<C>(
+                &mut deserialization_arena!(),
+                &identity_resolution_material,
+                &verification_material,
+            )
             .unwrap_err()
     );
 }
 
-fn do_bad_deserialize_tampered<
-    S: Fn(&mut CapacityLimitedVec<u8, NP_ADV_MAX_SECTION_LEN>),
-    A: Fn(&mut Vec<u8>),
->(
-    expected_error: MicEncryptedSectionDeserializationError,
-    mangle_section: S,
-    mangle_adv: A,
+#[derive(Debug, PartialEq)]
+enum DeserializeError {
+    IdentityResolutionOrDeserializationError(
+        IdentityResolutionOrDeserializationError<MicVerificationError>,
+    ),
+    DataElementParseError(DataElementParseError),
+}
+
+fn do_bad_deserialize_tampered(
+    expected_error: DeserializeError,
+    mangle_section: impl Fn(&mut CapacityLimitedVec<u8, NP_ADV_MAX_SECTION_LEN>),
+    mangle_adv: impl Fn(&mut Vec<u8>),
 ) {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let section_salt: V1Salt<CryptoProviderImpl> = [3; 16].into();
     let identity_type = EncryptedIdentityDataElementType::Private;
-    let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::new(&key_seed);
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
 
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new_for_testing(
+        .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new(
             identity_type,
             section_salt,
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -394,7 +422,7 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&v1_header, remaining).unwrap();
+    let sections = parse_sections(v1_header, remaining).unwrap();
     assert_eq!(1, sections.len());
 
     let section = sections.into_iter().next().unwrap();
@@ -405,22 +433,35 @@
         panic!("incorrect flavor");
     };
 
-    // generate a random key pair since we need _some_ public key
+    // generate a random key pair since we need _some_ public key in our discovery
+    // credential, even if it winds up going unused
     let key_pair = np_ed25519::KeyPair::<CryptoProviderImpl>::generate();
 
-    let crypto_material = HkdfCryptoMaterial::new(&key_seed, &metadata_key, key_pair.public());
-    let identity_resolution_material =
-        crypto_material.unsigned_identity_resolution_material::<CryptoProviderImpl>();
-    let verification_material =
-        crypto_material.unsigned_verification_material::<CryptoProviderImpl>();
+    let discovery_credential =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key())
+            .derive_v1_discovery_credential::<CryptoProviderImpl>();
 
-    assert_eq!(
-        expected_error,
-        contents
-            .try_deserialize::<CryptoProviderImpl>(
-                &identity_resolution_material,
-                &verification_material,
-            )
-            .unwrap_err()
-    );
+    let identity_resolution_material =
+        discovery_credential.unsigned_identity_resolution_material::<CryptoProviderImpl>();
+    let verification_material =
+        discovery_credential.unsigned_verification_material::<CryptoProviderImpl>();
+
+    match contents.try_resolve_identity_and_deserialize::<CryptoProviderImpl>(
+        &mut deserialization_arena!(),
+        &identity_resolution_material,
+        &verification_material,
+    ) {
+        Ok(section) => {
+            assert_eq!(
+                expected_error,
+                DeserializeError::DataElementParseError(
+                    section.collect_data_elements().unwrap_err()
+                )
+            );
+        }
+        Err(e) => assert_eq!(
+            expected_error,
+            DeserializeError::IdentityResolutionOrDeserializationError(e),
+        ),
+    };
 }
diff --git a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mod.rs b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mod.rs
index ad305f5..0afeb08 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mod.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/mod.rs
@@ -14,33 +14,124 @@
 
 use crate::{
     credential::v1::*,
+    deserialization_arena::DeserializationArena,
     extended::{
         deserialize::{
-            parse_non_identity_des, DecryptedSection, EncryptedIdentityMetadata, EncryptionInfo,
-            SectionContents, SectionMic, VerificationMode,
+            DecryptedSection, EncryptedIdentityMetadata, EncryptionInfo, SectionContents,
+            SectionMic, VerificationMode,
         },
         section_signature_payload::*,
-        to_array_view, METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN,
+        METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN,
     },
-    V1Header, NP_SVC_UUID,
+    MetadataKey, V1Header, NP_SVC_UUID,
 };
+
+#[cfg(any(feature = "devtools", test))]
+use alloc::vec::Vec;
+#[cfg(feature = "devtools")]
 use array_view::ArrayView;
+use core::fmt::Debug;
 use crypto_provider::{
-    aes::ctr::{AesCtr, AesCtrNonce, NonceAndCounter, AES_CTR_NONCE_LEN},
+    aes::ctr::{AesCtr, AesCtrNonce, NonceAndCounter},
     hmac::Hmac,
     CryptoProvider,
 };
 use np_hkdf::v1_salt::V1Salt;
 
+use super::ArenaOutOfSpace;
+
 #[cfg(test)]
 mod mic_decrypt_tests;
 #[cfg(test)]
 mod signature_decrypt_tests;
 
+/// Represents the contents of an encrypted section
+/// which are directly employed in identity resolution.
+/// This does not incorporate any information about credentials.
 #[derive(PartialEq, Eq, Debug)]
-pub(crate) struct SignatureEncryptedSection<'a> {
+pub(crate) struct SectionIdentityResolutionContents {
+    /// The ciphertext for the metadata key
+    pub(crate) metadata_key_ciphertext: [u8; METADATA_KEY_LEN],
+    /// The 12-byte cryptographic nonce which is derived from the encryption info
+    /// and the identity metadata for a particular section.
+    pub(crate) nonce: AesCtrNonce,
+}
+
+impl SectionIdentityResolutionContents {
+    /// Decrypt the contained metadata-key ciphertext buffer whose bytes of plaintext are, maybe, the
+    /// metadata key for an NP identity, as specified via pre-calculated cryptographic materials
+    /// stored in some [`SectionIdentityResolutionMaterial`].
+    ///
+    /// This method does not decrypt an entire section's ciphertext aside from the metadata key,
+    /// and so verification (MIC or signature) needs to be done elsewhere.
+    ///
+    /// Returns `Some` if decrypting the metadata-key ciphertext produces plaintext whose HMAC
+    /// matches the expected MAC. Otherwise, returns `None`.
+    pub(crate) fn try_match<C: CryptoProvider>(
+        &self,
+        identity_resolution_material: &SectionIdentityResolutionMaterial,
+    ) -> Option<IdentityMatch<C>> {
+        let mut decrypt_buf = self.metadata_key_ciphertext;
+        let aes_key = &identity_resolution_material.aes_key;
+        let mut cipher = C::AesCtr128::new(aes_key, NonceAndCounter::from_nonce(self.nonce));
+        cipher.apply_keystream(&mut decrypt_buf[..]);
+
+        let metadata_key_hmac_key: np_hkdf::NpHmacSha256Key<C> =
+            identity_resolution_material.metadata_key_hmac_key.into();
+        let expected_metadata_key_hmac = identity_resolution_material.expected_metadata_key_hmac;
+        metadata_key_hmac_key.verify_hmac(&decrypt_buf[..], expected_metadata_key_hmac).ok().map(
+            move |_| IdentityMatch {
+                cipher,
+                metadata_key_plaintext: MetadataKey(decrypt_buf),
+                nonce: self.nonce,
+            },
+        )
+    }
+}
+
+/// Carries data about an identity "match" for a particular section
+/// against some particular V1 identity-resolution crypto-materials.
+pub(crate) struct IdentityMatch<C: CryptoProvider> {
+    /// Decrypted metadata key ciphertext
+    metadata_key_plaintext: MetadataKey,
+    /// The AES-Ctr nonce to be used in section decryption and verification
+    nonce: AesCtrNonce,
+    /// The state of the AES-Ctr cipher after successfully decrypting
+    /// the metadata key ciphertext. May be used to decrypt the remainder
+    /// of the section ciphertext.
+    cipher: C::AesCtr128,
+}
+
+/// Maximum length of a section's contents, after the metadata-key.
+#[allow(unused)]
+const MAX_SECTION_CONTENTS_LEN: usize = NP_ADV_MAX_SECTION_LEN - METADATA_KEY_LEN;
+
+/// Bare, decrypted contents from an encrypted section,
+/// including the decrypted metadata key and the decrypted section ciphertext.
+/// At this point, verification of the plaintext contents has not yet been performed.
+pub(crate) struct RawDecryptedSection<'adv> {
+    pub(crate) metadata_key_plaintext: MetadataKey,
+    pub(crate) nonce: AesCtrNonce,
+    pub(crate) plaintext_contents: &'adv [u8],
+}
+
+#[cfg(feature = "devtools")]
+impl<'adv> RawDecryptedSection<'adv> {
+    pub(crate) fn to_raw_bytes(&self) -> ArrayView<u8, NP_ADV_MAX_SECTION_LEN> {
+        let mut result = Vec::new();
+        result.extend_from_slice(&self.metadata_key_plaintext.0);
+        result.extend_from_slice(self.plaintext_contents);
+        // Won't panic because of the involved lengths
+        ArrayView::try_from_slice(&result).unwrap()
+    }
+}
+
+/// Represents the contents of an encrypted section,
+/// independent of the encryption type.
+#[derive(PartialEq, Eq, Debug)]
+pub(crate) struct EncryptedSectionContents<'a> {
     pub(crate) section_header: u8,
-    pub(crate) adv_header: &'a V1Header,
+    pub(crate) adv_header: V1Header,
     pub(crate) encryption_info: EncryptionInfo,
     pub(crate) identity: EncryptedIdentityMetadata,
     /// All ciphertext (Contents of identity DE + all DEs)
@@ -48,83 +139,134 @@
     pub(crate) all_ciphertext: &'a [u8],
 }
 
-impl<'a> SignatureEncryptedSection<'a> {
-    /// Decrypt the ciphertext and check that the first [METADATA_KEY_LEN] bytes match the expected HMAC.
-    /// The remaining data is not verified.
+impl<'a> EncryptedSectionContents<'a> {
+    /// Constructs a representation of the contents of an encrypted V1 section
+    /// from the advertisement header, the section header, information about
+    /// the encryption used for identity verification, identity metadata,
+    /// and the entire section contents as an undecrypted ciphertext.
     ///
-    /// Returns `Ok` if the section was an identity match.
-    /// Otherwise, returns `Err`.
-    pub(crate) fn try_decrypt<P: CryptoProvider>(
-        &self,
-        identity_resolution_material: &SignedSectionIdentityResolutionMaterial,
-    ) -> Result<ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, SignatureEncryptedSectionDeserializationError>
-    {
-        let salt: V1Salt<P> = self.encryption_info.salt().into();
-        let (mut buf, mut cipher) = try_decrypt_metadata_key_prefix::<P>(
-            self.all_ciphertext,
-            salt.derive(Some(self.identity.offset)).expect("AES-CTR nonce is a valid HKDF size"),
-            identity_resolution_material.as_raw_resolution_material(),
-        )
-        .ok_or(SignatureEncryptedSectionDeserializationError::MetadataKeyMacMismatch)?;
-
-        // decrypt everything else
-        cipher.decrypt(&mut buf[METADATA_KEY_LEN..]);
-
-        Ok(to_array_view(buf))
+    /// # Panics
+    /// If `all_ciphertext` is greater than `NP_ADV_MAX_SECTION_LEN` bytes,
+    /// or less than `METADATA_KEY_LEN` bytes.
+    pub(crate) fn new(
+        adv_header: V1Header,
+        section_header: u8,
+        encryption_info: EncryptionInfo,
+        identity: EncryptedIdentityMetadata,
+        all_ciphertext: &'a [u8],
+    ) -> Self {
+        assert!(all_ciphertext.len() >= METADATA_KEY_LEN);
+        assert!(all_ciphertext.len() <= NP_ADV_MAX_SECTION_LEN);
+        Self { adv_header, section_header, encryption_info, identity, all_ciphertext }
     }
 
-    /// Try deserializing into a [Section].
-    ///
-    /// Returns an error if the credential is incorrect or if the section data is malformed.
+    /// Gets the salt for this encrypted section
+    pub(crate) fn salt<C: CryptoProvider>(&self) -> V1Salt<C> {
+        self.encryption_info.salt().into()
+    }
+
+    /// Constructs a cryptographic nonce for this encrypted section
+    /// based on the contained salt.
+    pub(crate) fn compute_nonce<C: CryptoProvider>(&self) -> AesCtrNonce {
+        self.salt::<C>()
+            .derive(Some(self.identity.offset))
+            .expect("AES-CTR nonce is a valid HKDF size")
+    }
+
+    /// Constructs some cryptographic contents for section identity-resolution
+    /// out of the entire contents of this encrypted section.
+    pub(crate) fn compute_identity_resolution_contents<C: CryptoProvider>(
+        &self,
+    ) -> SectionIdentityResolutionContents {
+        let nonce = self.compute_nonce::<C>();
+        let metadata_key_ciphertext: [u8; METADATA_KEY_LEN] =
+            self.all_ciphertext[..METADATA_KEY_LEN].try_into().unwrap();
+
+        SectionIdentityResolutionContents { nonce, metadata_key_ciphertext }
+    }
+
+    /// Given an identity-match, decrypts the ciphertext in this encrypted section
+    /// and returns the raw bytes of the decrypted plaintext.
+    pub(crate) fn decrypt_ciphertext<C: CryptoProvider>(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        mut identity_match: IdentityMatch<C>,
+    ) -> Result<RawDecryptedSection<'a>, ArenaOutOfSpace> {
+        // Fill decrypt_buf with the ciphertext after the metadata key
+        let decrypt_buf =
+            arena.allocate(u8::try_from(self.all_ciphertext.len() - METADATA_KEY_LEN).expect(
+                "all_ciphertext.len() must be in [METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN]",
+            ))?;
+        decrypt_buf.copy_from_slice(&self.all_ciphertext[METADATA_KEY_LEN..]);
+
+        // Decrypt everything after the metadata key
+        identity_match.cipher.apply_keystream(decrypt_buf);
+
+        Ok(RawDecryptedSection {
+            metadata_key_plaintext: identity_match.metadata_key_plaintext,
+            nonce: identity_match.nonce,
+            plaintext_contents: decrypt_buf,
+        })
+    }
+
+    /// Try decrypting into some raw bytes given some raw identity-resolution material.
+    #[cfg(feature = "devtools")]
+    pub(crate) fn try_resolve_identity_and_decrypt<P: CryptoProvider>(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        identity_resolution_material: &SectionIdentityResolutionMaterial,
+    ) -> Option<Result<ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, ArenaOutOfSpace>> {
+        let identity_resolution_contents = self.compute_identity_resolution_contents::<P>();
+        identity_resolution_contents.try_match(identity_resolution_material).map(|identity_match| {
+            let decrypted_section = self.decrypt_ciphertext::<P>(arena, identity_match)?;
+            Ok(decrypted_section.to_raw_bytes())
+        })
+    }
+}
+
+/// An encrypted section which is verified using a ed25519 signature
+#[derive(PartialEq, Eq, Debug)]
+pub(crate) struct SignatureEncryptedSection<'a> {
+    pub(crate) contents: EncryptedSectionContents<'a>,
+}
+
+impl<'a> SignatureEncryptedSection<'a> {
+    /// Try deserializing into a [`DecryptedSection`] given an identity-match
+    /// with some paired verification material for the matched identity.
     pub(crate) fn try_deserialize<P>(
         &self,
-        identity_resolution_material: &SignedSectionIdentityResolutionMaterial,
+        arena: &mut DeserializationArena<'a>,
+        identity_match: IdentityMatch<P>,
         verification_material: &SignedSectionVerificationMaterial,
-    ) -> Result<DecryptedSection, SignatureEncryptedSectionDeserializationError>
+    ) -> Result<DecryptedSection<'a>, DeserializationError<SignatureVerificationError>>
     where
         P: CryptoProvider,
     {
-        let raw_salt = self.encryption_info.salt();
-        let salt: V1Salt<P> = raw_salt.into();
-        let plaintext = self.try_decrypt::<P>(identity_resolution_material)?;
-
-        // won't panic: plaintext length is at least METADATA_KEY_LEN
-        let (metadata_key, remaining) = plaintext.as_slice().split_at(METADATA_KEY_LEN);
-        let metadata_key =
-            metadata_key.try_into().expect("slice is the same length as the desired array");
+        let raw_decrypted = self.contents.decrypt_ciphertext(arena, identity_match)?;
+        let metadata_key = raw_decrypted.metadata_key_plaintext;
+        let nonce = raw_decrypted.nonce;
+        let remaining = raw_decrypted.plaintext_contents;
 
         if remaining.len() < crypto_provider::ed25519::SIGNATURE_LENGTH {
-            return Err(SignatureEncryptedSectionDeserializationError::SignatureMissing);
+            return Err(SignatureVerificationError::SignatureMissing.into());
         }
 
         // should not panic due to above check
         let (non_identity_des, sig) =
             remaining.split_at(remaining.len() - crypto_provider::ed25519::SIGNATURE_LENGTH);
 
-        // de offset 2 because of leading encryption info and identity DEs
-        let (_, ref_des) = parse_non_identity_des(2)(non_identity_des)
-            .map_err(|_| SignatureEncryptedSectionDeserializationError::DeParseError)?;
-
         // All implementations only check for 64 bytes, and this will always result in a 64 byte signature.
         let expected_signature =
             np_ed25519::Signature::<P>::try_from(sig).expect("Signature is always 64 bytes.");
 
-        let nonce = salt
-            .derive::<{ AES_CTR_NONCE_LEN }>(Some(self.identity.offset))
-            .expect("AES-CTR nonce is a valid HKDF length");
-
-        // all other plaintext, except for the signature.
-        // Won't panic because we know we just parsed the sig de from this,
-        // and we're using the as-parsed lengths
-        let remainder = &plaintext.as_slice()[..plaintext.len() - sig.len()];
-
         let section_signature_payload = SectionSignaturePayload::from_deserialized_parts(
-            self.adv_header.header_byte,
-            self.section_header,
-            &self.encryption_info.bytes,
+            self.contents.adv_header.header_byte,
+            self.contents.section_header,
+            &self.contents.encryption_info.bytes,
             &nonce,
-            self.identity.header_bytes,
-            remainder,
+            self.contents.identity.header_bytes,
+            metadata_key,
+            non_identity_des,
         );
 
         let public_key = verification_material.signature_verification_public_key();
@@ -133,161 +275,221 @@
             // Length of the payload should fit in the signature verification buffer.
             debug_assert!(e != np_ed25519::SignatureVerificationError::PayloadTooBig);
 
-            SignatureEncryptedSectionDeserializationError::SignatureMismatch
+            SignatureVerificationError::SignatureMismatch
         })?;
 
+        let salt = self.contents.salt::<P>();
+
         Ok(DecryptedSection::new(
-            self.identity.identity_type,
+            self.contents.identity.identity_type,
             VerificationMode::Signature,
             metadata_key,
-            raw_salt,
-            SectionContents::new(self.section_header, &ref_des),
+            salt.into(),
+            // de offset 2 because of leading encryption info and identity DEs
+            SectionContents::new(self.contents.section_header, non_identity_des, 2),
         ))
     }
+
+    /// Try decrypting into some raw bytes given some raw signed crypto-material.
+    #[cfg(feature = "devtools")]
+    pub(crate) fn try_resolve_identity_and_decrypt<P: CryptoProvider>(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        identity_resolution_material: &SignedSectionIdentityResolutionMaterial,
+    ) -> Option<Result<ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, ArenaOutOfSpace>> {
+        self.contents.try_resolve_identity_and_decrypt::<P>(
+            arena,
+            identity_resolution_material.as_raw_resolution_material(),
+        )
+    }
+
+    /// Try deserializing into a [Section] given some raw signed crypto-material.
+    #[cfg(test)]
+    pub(crate) fn try_resolve_identity_and_deserialize<P: CryptoProvider>(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        identity_resolution_material: &SignedSectionIdentityResolutionMaterial,
+        verification_material: &SignedSectionVerificationMaterial,
+    ) -> Result<
+        DecryptedSection,
+        IdentityResolutionOrDeserializationError<SignatureVerificationError>,
+    > {
+        let section_identity_resolution_contents =
+            self.contents.compute_identity_resolution_contents::<P>();
+        match section_identity_resolution_contents
+            .try_match::<P>(identity_resolution_material.as_raw_resolution_material())
+        {
+            Some(identity_match) => self
+                .try_deserialize(arena, identity_match, verification_material)
+                .map_err(|e| e.into()),
+            None => Err(IdentityResolutionOrDeserializationError::IdentityMatchingError),
+        }
+    }
 }
 
-/// Should not be exposed publicly as it's too detailed.
+/// An error when attempting to resolve an identity and then
+/// attempt to deserialize an encrypted advertisement.
+///
+/// This should not be exposed publicly, since it's too
+/// detailed.
+#[cfg(test)]
 #[derive(Debug, PartialEq, Eq)]
-pub(crate) enum SignatureEncryptedSectionDeserializationError {
-    /// The decrypted metadata key did not have the right MAC
-    MetadataKeyMacMismatch,
+pub(crate) enum IdentityResolutionOrDeserializationError<V: VerificationError> {
+    /// Failed to match the encrypted adv to an identity
+    IdentityMatchingError,
+    /// Failed to deserialize the encrypted adv after matching the identity
+    DeserializationError(DeserializationError<V>),
+}
+
+#[cfg(test)]
+impl<V: VerificationError> From<DeserializationError<V>>
+    for IdentityResolutionOrDeserializationError<V>
+{
+    fn from(deserialization_error: DeserializationError<V>) -> Self {
+        Self::DeserializationError(deserialization_error)
+    }
+}
+
+#[cfg(test)]
+impl<V: VerificationError> From<V> for IdentityResolutionOrDeserializationError<V> {
+    fn from(verification_error: V) -> Self {
+        Self::DeserializationError(DeserializationError::VerificationError(verification_error))
+    }
+}
+
+/// An error when attempting to deserialize an encrypted advertisement,
+/// assuming that we already have an identity-match.
+///
+/// This should not be exposed publicly, since it's too
+/// detailed.
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) enum DeserializationError<V: VerificationError> {
+    /// Verification failed
+    VerificationError(V),
+    /// The given arena ran out of space
+    ArenaOutOfSpace,
+}
+
+impl<V: VerificationError> From<ArenaOutOfSpace> for DeserializationError<V> {
+    fn from(_: ArenaOutOfSpace) -> Self {
+        Self::ArenaOutOfSpace
+    }
+}
+
+impl<V: VerificationError> From<V> for DeserializationError<V> {
+    fn from(verification_error: V) -> Self {
+        Self::VerificationError(verification_error)
+    }
+}
+
+/// Common trait bound for errors which arise during
+/// verification of encrypted section contents.
+///
+/// Implementors should not be exposed publicly, since it's too
+/// detailed.
+pub(crate) trait VerificationError: Debug + PartialEq + Eq {}
+
+/// An error when attempting to verify a signature
+#[derive(Debug, PartialEq, Eq)]
+pub(crate) enum SignatureVerificationError {
     /// The provided signature did not match the calculated signature
     SignatureMismatch,
     /// The provided signature is missing
     SignatureMissing,
-    /// Decrypted DEs could not be parsed, possibly because the ciphertext was tampered with, as
-    /// DE parsing has to be done before the signature is verified
-    DeParseError,
 }
 
+impl VerificationError for SignatureVerificationError {}
+
+/// An encrypted section whose contents are verified to match a message integrity code (MIC)
 #[derive(PartialEq, Eq, Debug)]
 pub(crate) struct MicEncryptedSection<'a> {
-    pub(crate) section_header: u8,
-    pub(crate) adv_header: &'a V1Header,
-    pub(crate) encryption_info: EncryptionInfo,
+    pub(crate) contents: EncryptedSectionContents<'a>,
     pub(crate) mic: SectionMic,
-    pub(crate) identity: EncryptedIdentityMetadata,
-    /// All ciphertext (Contents of identity DE + all DEs).
-    /// Length must be in `[METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN]`.
-    pub(crate) all_ciphertext: &'a [u8],
 }
 
 impl<'a> MicEncryptedSection<'a> {
-    /// Attempt to decrypt and verify the encrypted portion of the section with the provided
-    /// precalculated cryptographic material for an MIC-encrypted section.
-    ///
-    /// If successful, returns a buffer containing the decrypted plaintext.
-    pub(crate) fn try_decrypt<P: CryptoProvider>(
-        &self,
-        identity_resolution_material: &UnsignedSectionIdentityResolutionMaterial,
-        verification_material: &UnsignedSectionVerificationMaterial,
-    ) -> Result<ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, MicEncryptedSectionDeserializationError>
-    {
-        let salt: V1Salt<P> = self.encryption_info.salt().into();
-        let nonce =
-            salt.derive(Some(self.identity.offset)).expect("AES-CTR nonce is a valid HKDF size");
-
-        let (mut buf, mut cipher) = try_decrypt_metadata_key_prefix::<P>(
-            self.all_ciphertext,
-            nonce,
-            identity_resolution_material.as_raw_resolution_material(),
-        )
-        .ok_or(MicEncryptedSectionDeserializationError::MetadataKeyMacMismatch)?;
-
-        // if mic is ok, the section was generated by someone holding at least the shared credential
-        let mut mic_hmac = verification_material.mic_hmac_key::<P>().build_hmac();
-        mic_hmac.update(&NP_SVC_UUID);
-        mic_hmac.update(&[self.adv_header.header_byte]);
-        mic_hmac.update(&[self.section_header]);
-        mic_hmac.update(&self.encryption_info.bytes);
-        mic_hmac.update(&nonce);
-        mic_hmac.update(&self.identity.header_bytes);
-        mic_hmac.update(self.all_ciphertext);
-        mic_hmac
-            // adv only contains first 16 bytes of HMAC
-            .verify_truncated_left(&self.mic.mic)
-            .map_err(|_e| MicEncryptedSectionDeserializationError::MicMismatch)?;
-
-        // decrypt everything else
-        cipher.decrypt(&mut buf[METADATA_KEY_LEN..]);
-
-        Ok(to_array_view(buf))
-    }
-
-    /// Try deserializing into a [Section].
+    /// Try deserializing into a [`DecryptedSection`].
     ///
     /// Returns an error if the credential is incorrect or if the section data is malformed.
     pub(crate) fn try_deserialize<P>(
         &self,
-        identity_resolution_material: &UnsignedSectionIdentityResolutionMaterial,
+        arena: &mut DeserializationArena<'a>,
+        identity_match: IdentityMatch<P>,
         verification_material: &UnsignedSectionVerificationMaterial,
-    ) -> Result<DecryptedSection, MicEncryptedSectionDeserializationError>
+    ) -> Result<DecryptedSection<'a>, DeserializationError<MicVerificationError>>
     where
         P: CryptoProvider,
     {
-        let plaintext =
-            self.try_decrypt::<P>(identity_resolution_material, verification_material)?;
-        // won't panic: plaintext is at least METADATA_KEY_LEN long
-        let (metadata_key, remaining_des) = plaintext.as_slice().split_at(METADATA_KEY_LEN);
-        let metadata_key =
-            metadata_key.try_into().expect("slice is the same length as the desired array");
-        // offset 2 for encryption info and identity DEs
-        let (_, ref_des) = parse_non_identity_des(2)(remaining_des)
-            .map_err(|_| MicEncryptedSectionDeserializationError::DeParseError)?;
+        let raw_decrypted = self.contents.decrypt_ciphertext(arena, identity_match)?;
+        let metadata_key = raw_decrypted.metadata_key_plaintext;
+        let nonce = raw_decrypted.nonce;
+        let remaining_des = raw_decrypted.plaintext_contents;
+
+        // if mic is ok, the section was generated by someone holding at least the shared credential
+        let mut mic_hmac = verification_material.mic_hmac_key::<P>().build_hmac();
+        mic_hmac.update(&NP_SVC_UUID);
+        mic_hmac.update(&[self.contents.adv_header.header_byte]);
+        mic_hmac.update(&[self.contents.section_header]);
+        mic_hmac.update(&self.contents.encryption_info.bytes);
+        mic_hmac.update(&nonce);
+        mic_hmac.update(&self.contents.identity.header_bytes);
+        mic_hmac.update(self.contents.all_ciphertext);
+        mic_hmac
+            // adv only contains first 16 bytes of HMAC
+            .verify_truncated_left(&self.mic.mic)
+            .map_err(|_e| MicVerificationError::MicMismatch)?;
+
+        let salt = self.contents.salt::<P>();
 
         Ok(DecryptedSection::new(
-            self.identity.identity_type,
+            self.contents.identity.identity_type,
             VerificationMode::Mic,
             metadata_key,
-            self.encryption_info.salt(),
-            SectionContents::new(self.section_header, &ref_des),
+            salt.into(),
+            // offset 2 for encryption info and identity DEs
+            SectionContents::new(self.contents.section_header, remaining_des, 2),
         ))
     }
+
+    /// Try decrypting into some raw bytes given some raw unsigned crypto-material.
+    #[cfg(feature = "devtools")]
+    pub(crate) fn try_resolve_identity_and_decrypt<P: CryptoProvider>(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        identity_resolution_material: &UnsignedSectionIdentityResolutionMaterial,
+    ) -> Option<Result<ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, ArenaOutOfSpace>> {
+        self.contents.try_resolve_identity_and_decrypt::<P>(
+            arena,
+            identity_resolution_material.as_raw_resolution_material(),
+        )
+    }
+
+    /// Try deserializing into a [Section] given some raw unsigned crypto-material.
+    #[cfg(test)]
+    pub(crate) fn try_resolve_identity_and_deserialize<P: CryptoProvider>(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        identity_resolution_material: &UnsignedSectionIdentityResolutionMaterial,
+        verification_material: &UnsignedSectionVerificationMaterial,
+    ) -> Result<DecryptedSection, IdentityResolutionOrDeserializationError<MicVerificationError>>
+    {
+        let section_identity_resolution_contents =
+            self.contents.compute_identity_resolution_contents::<P>();
+        match section_identity_resolution_contents
+            .try_match::<P>(identity_resolution_material.as_raw_resolution_material())
+        {
+            Some(identity_match) => self
+                .try_deserialize(arena, identity_match, verification_material)
+                .map_err(|e| e.into()),
+            None => Err(IdentityResolutionOrDeserializationError::IdentityMatchingError),
+        }
+    }
 }
 
-/// Should not be exposed publicly as it's too detailed.
 #[derive(Debug, PartialEq, Eq)]
-pub(crate) enum MicEncryptedSectionDeserializationError {
-    /// Plaintext metadata key did not have the expected MAC
-    MetadataKeyMacMismatch,
+pub(crate) enum MicVerificationError {
     /// Calculated MIC did not match MIC from section
     MicMismatch,
-    /// Parsing of decrypted DEs failed
-    DeParseError,
 }
 
-/// Decrypt a ciphertext buffer whose first [METADATA_KEY_LEN] bytes of plaintext are, maybe, the
-/// metadata key for an NP identity.
-///
-/// Returns `Some` if decrypting the first [METADATA_KEY_LEN] bytes produces plaintext whose HMAC
-/// matches the expected MAC. Only the first [METADATA_KEY_LEN] bytes are decrypted. The cipher is
-/// returned along with the buffer as it is in the appropriate state to continue decrypting the
-/// remaining ciphertext.
-///
-/// Returns `None` if the plaintext does not match the expected HMAC.
-///
-/// # Panics
-///
-/// Panics if `ciphertext`'s length is not in `[METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN]`.
-#[allow(clippy::type_complexity)]
-fn try_decrypt_metadata_key_prefix<C: CryptoProvider>(
-    ciphertext: &[u8],
-    nonce: AesCtrNonce,
-    identity_resolution_material: &SectionIdentityResolutionMaterial,
-) -> Option<(tinyvec::ArrayVec<[u8; NP_ADV_MAX_SECTION_LEN]>, C::AesCtr128)> {
-    assert!(ciphertext.len() <= NP_ADV_MAX_SECTION_LEN);
-    let mut decrypt_buf = tinyvec::ArrayVec::<[u8; NP_ADV_MAX_SECTION_LEN]>::new();
-    let aes_key = &identity_resolution_material.aes_key;
-    let mut cipher = C::AesCtr128::new(aes_key, NonceAndCounter::from_nonce(nonce));
-
-    decrypt_buf.extend_from_slice(ciphertext);
-    cipher.decrypt(&mut decrypt_buf[..METADATA_KEY_LEN]);
-
-    let metadata_key_hmac_key: np_hkdf::NpHmacSha256Key<C> =
-        identity_resolution_material.metadata_key_hmac_key.into();
-    let expected_metadata_key_hmac = identity_resolution_material.expected_metadata_key_hmac;
-    metadata_key_hmac_key
-        .verify_hmac(&decrypt_buf[..METADATA_KEY_LEN], expected_metadata_key_hmac)
-        .ok()
-        .map(|_| (decrypt_buf, cipher))
-}
+impl VerificationError for MicVerificationError {}
diff --git a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/signature_decrypt_tests.rs b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/signature_decrypt_tests.rs
index b78cad9..10fc46b 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/signature_decrypt_tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/encrypted_section/signature_decrypt_tests.rs
@@ -14,8 +14,12 @@
 
 extern crate std;
 
+use crate::deserialization_arena;
 use crate::extended::data_elements::TxPowerDataElement;
-use crate::extended::deserialize::{DecryptedSection, EncryptionInfo, RawV1Salt};
+use crate::extended::deserialize::{
+    DataElementParseError, DecryptedSection, EncryptionInfo, RawV1Salt, SectionContents,
+};
+use crate::extended::serialize::AdvertisementType;
 use crate::shared_data::TxPower;
 use crate::{
     credential::v1::*,
@@ -23,23 +27,22 @@
     extended::{
         deserialize::{
             encrypted_section::{
-                SignatureEncryptedSection, SignatureEncryptedSectionDeserializationError,
+                EncryptedSectionContents, IdentityResolutionOrDeserializationError,
+                SignatureEncryptedSection, SignatureVerificationError,
             },
             parse_sections,
             test_stubs::IntermediateSectionExt,
-            CiphertextSection, EncryptedIdentityMetadata, OffsetDataElement, SectionContents,
-            VerificationMode,
+            CiphertextSection, DataElement, EncryptedIdentityMetadata, VerificationMode,
         },
         section_signature_payload::*,
         serialize::{
-            AdvBuilder, CapacityLimitedVec, SignedEncrypted, SingleTypeDataElement,
+            AdvBuilder, CapacityLimitedVec, SignedEncryptedSectionEncoder, SingleTypeDataElement,
             WriteDataElement,
         },
         NP_ADV_MAX_SECTION_LEN,
     },
-    parse_adv_header, AdvHeader, Section,
+    parse_adv_header, AdvHeader, MetadataKey, Section,
 };
-use array_view::ArrayView;
 use crypto_provider::{aes::ctr::AesCtrNonce, CryptoProvider};
 use crypto_provider_default::CryptoProviderImpl;
 use np_hkdf::v1_salt;
@@ -51,7 +54,7 @@
 
 #[test]
 fn deserialize_signature_encrypted_correct_keys() {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let raw_salt = RawV1Salt([3; 16]);
     let section_salt = V1Salt::<CryptoProviderImpl>::from(raw_salt);
@@ -59,15 +62,16 @@
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
     let key_pair = KeyPair::generate();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
 
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new(
+        .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new(
             identity_type,
             V1Salt::from(*section_salt.as_array_ref()),
-            &metadata_key,
-            &key_pair,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -86,7 +90,7 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&adv_header, remaining).unwrap();
+    let sections = parse_sections(adv_header, remaining).unwrap();
     assert_eq!(1, sections.len());
 
     let section = sections.into_iter().next().unwrap();
@@ -105,36 +109,44 @@
     let section_len = 19 + 2 + 16 + 2 + 64;
     assert_eq!(
         &SignatureEncryptedSection {
-            section_header: section_len,
-            adv_header: &adv_header,
-            encryption_info: EncryptionInfo { bytes: encryption_info_bytes },
-            identity: EncryptedIdentityMetadata {
-                header_bytes: [0x90, 0x1],
-                offset: 1.into(),
-                identity_type: EncryptedIdentityDataElementType::Private,
+            contents: EncryptedSectionContents {
+                section_header: section_len,
+                adv_header,
+                encryption_info: EncryptionInfo { bytes: encryption_info_bytes },
+                identity: EncryptedIdentityMetadata {
+                    header_bytes: [0x90, 0x1],
+                    offset: 1.into(),
+                    identity_type: EncryptedIdentityDataElementType::Private,
+                },
+                // adv header + salt + section header + encryption info + identity header
+                all_ciphertext: &adv.as_slice()[1 + 1 + 19 + 2..],
             },
-            // adv header + salt + section header + encryption info + identity header
-            all_ciphertext: &adv.as_slice()[1 + 1 + 19 + 2..],
         },
         contents
     );
 
     // plaintext is correct
     {
-        let crypto_material = MinimumFootprintV1CryptoMaterial::new::<CryptoProviderImpl>(
+        let crypto_material = V1DiscoveryCredential::new::<CryptoProviderImpl>(
             key_seed,
-            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key),
+            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
+            key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
             key_pair.public(),
         );
         let signed_identity_resolution_material =
             crypto_material.signed_identity_resolution_material::<CryptoProviderImpl>();
-        let decrypted = contents
-            .try_decrypt::<CryptoProviderImpl>(&signed_identity_resolution_material)
+        let identity_resolution_contents =
+            contents.contents.compute_identity_resolution_contents::<CryptoProviderImpl>();
+        let identity_match = identity_resolution_contents
+            .try_match::<CryptoProviderImpl>(
+                &signed_identity_resolution_material.into_raw_resolution_material(),
+            )
             .unwrap();
 
-        let mut expected = metadata_key.as_slice().to_vec();
-        // battery de
+        let mut arena = deserialization_arena!();
+        let decrypted = contents.contents.decrypt_ciphertext(&mut arena, identity_match).unwrap();
+
+        let mut expected = Vec::new();
         expected.extend_from_slice(txpower_de.de_header().serialize().as_slice());
         txpower_de.write_de_contents(&mut expected);
 
@@ -151,20 +163,22 @@
             &encryption_info,
             &nonce,
             [0x90, 0x1],
+            metadata_key,
             &expected,
         );
 
         expected.extend_from_slice(&sig_payload.sign(&key_pair).to_bytes());
-
-        assert_eq!(&expected, decrypted.as_slice());
+        assert_eq!(metadata_key, decrypted.metadata_key_plaintext);
+        assert_eq!(nonce, decrypted.nonce);
+        assert_eq!(&expected, decrypted.plaintext_contents);
     }
 
     // deserialization to Section works
     {
-        let crypto_material = MinimumFootprintV1CryptoMaterial::new::<CryptoProviderImpl>(
+        let crypto_material = V1DiscoveryCredential::new::<CryptoProviderImpl>(
             key_seed,
-            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key),
+            key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
+            key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
             key_pair.public(),
         );
         let signed_identity_resolution_material =
@@ -172,8 +186,10 @@
         let signed_verification_material =
             crypto_material.signed_verification_material::<CryptoProviderImpl>();
 
+        let mut arena = deserialization_arena!();
         let section = contents
-            .try_deserialize::<CryptoProviderImpl>(
+            .try_resolve_identity_and_deserialize::<CryptoProviderImpl>(
+                &mut arena,
                 &signed_identity_resolution_material,
                 &signed_verification_material,
             )
@@ -187,27 +203,23 @@
                 raw_salt,
                 SectionContents {
                     section_header: 19 + 2 + 16 + 1 + 1 + 64,
-                    // battery DE
-                    de_data: ArrayView::try_from_slice(&[7]).unwrap(),
-                    data_elements: vec![OffsetDataElement {
-                        offset: 2.into(),
-                        de_type: 0x05_u8.into(),
-                        start_of_contents: 0,
-                        contents_len: 1,
-                    }],
+                    data_element_start_offset: 2,
+                    de_region_excl_identity:
+                        &[txpower_de.de_header().serialize().as_slice(), &[7],].concat(),
                 },
             ),
             section
         );
+        let data_elements = section.collect_data_elements().unwrap();
+        assert_eq!(
+            data_elements,
+            &[DataElement { offset: 2.into(), de_type: 0x05_u8.into(), contents: &[7] }]
+        );
 
         assert_eq!(
-            vec![(
-                v1_salt::DataElementOffset::from(2_usize),
-                TxPowerDataElement::DE_TYPE,
-                vec![7u8]
-            )],
-            section
-                .data_elements()
+            vec![(v1_salt::DataElementOffset::from(2_u8), TxPowerDataElement::DE_TYPE, vec![7u8])],
+            data_elements
+                .into_iter()
                 .map(|de| (de.offset(), de.de_type(), de.contents().to_vec()))
                 .collect::<Vec<_>>()
         );
@@ -218,7 +230,7 @@
 fn deserialize_signature_encrypted_incorrect_aes_key_error() {
     // bad aes key -> bad metadata key plaintext
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        SignatureEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        IdentityResolutionOrDeserializationError::IdentityMatchingError,
         Some([0xFF; 16].into()),
         None,
         None,
@@ -230,7 +242,7 @@
 fn deserialize_signature_encrypted_incorrect_metadata_key_hmac_key_error() {
     // bad metadata key hmac key -> bad calculated metadata key mac
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        SignatureEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        IdentityResolutionOrDeserializationError::IdentityMatchingError,
         None,
         Some([0xFF; 32].into()),
         None,
@@ -242,7 +254,7 @@
 fn deserialize_signature_encrypted_incorrect_expected_metadata_key_hmac_error() {
     // bad expected metadata key mac
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        SignatureEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        IdentityResolutionOrDeserializationError::IdentityMatchingError,
         None,
         None,
         Some([0xFF; 32]),
@@ -254,7 +266,7 @@
 fn deserialize_signature_encrypted_incorrect_pub_key_error() {
     // a random pub key will lead to signature mismatch
     do_bad_deserialize_params::<CryptoProviderImpl>(
-        SignatureEncryptedSectionDeserializationError::SignatureMismatch,
+        SignatureVerificationError::SignatureMismatch.into(),
         None,
         None,
         None,
@@ -266,7 +278,9 @@
 fn deserialize_signature_encrypted_incorrect_salt_error() {
     // bad salt -> bad iv -> bad metadata key plaintext
     do_bad_deserialize_tampered(
-        SignatureEncryptedSectionDeserializationError::MetadataKeyMacMismatch,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            IdentityResolutionOrDeserializationError::IdentityMatchingError,
+        ),
         None,
         |_| {},
         |adv_mut| adv_mut[5..21].copy_from_slice(&[0xFF; 16]),
@@ -276,7 +290,9 @@
 #[test]
 fn deserialize_signature_encrypted_tampered_signature_error() {
     do_bad_deserialize_tampered(
-        SignatureEncryptedSectionDeserializationError::SignatureMismatch,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            SignatureVerificationError::SignatureMismatch.into(),
+        ),
         None,
         |_| {},
         // flip a bit in the middle of the signature
@@ -290,7 +306,9 @@
 #[test]
 fn deserialize_signature_encrypted_tampered_ciphertext_error() {
     do_bad_deserialize_tampered(
-        SignatureEncryptedSectionDeserializationError::SignatureMismatch,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            SignatureVerificationError::SignatureMismatch.into(),
+        ),
         None,
         |_| {},
         // flip a bit outside of the signature
@@ -305,7 +323,9 @@
 fn deserialize_signature_encrypted_missing_signature_de_error() {
     let section_len = 19 + 2 + 16 + 1 + 1;
     do_bad_deserialize_tampered(
-        SignatureEncryptedSectionDeserializationError::SignatureMissing,
+        DeserializeError::IdentityResolutionOrDeserializationError(
+            SignatureVerificationError::SignatureMissing.into(),
+        ),
         Some(section_len),
         |_| {},
         |adv_mut| {
@@ -320,7 +340,7 @@
 #[test]
 fn deserialize_signature_encrypted_des_wont_parse() {
     do_bad_deserialize_tampered(
-        SignatureEncryptedSectionDeserializationError::DeParseError,
+        DeserializeError::DataElementParseError(DataElementParseError::UnexpectedDataAfterEnd),
         Some(19 + 2 + 16 + 1 + 1 + 64 + 1),
         // add an impossible DE
         |section| section.try_push(0xFF).unwrap(),
@@ -331,28 +351,29 @@
 /// Attempt a deserialization that will fail when using the provided parameters for decryption only.
 /// `None` means use the correct value for that parameter.
 fn do_bad_deserialize_params<C: CryptoProvider>(
-    error: SignatureEncryptedSectionDeserializationError,
+    error: IdentityResolutionOrDeserializationError<SignatureVerificationError>,
     aes_key: Option<crypto_provider::aes::Aes128Key>,
     metadata_key_hmac_key: Option<np_hkdf::NpHmacSha256Key<C>>,
     expected_metadata_key_hmac: Option<[u8; 32]>,
     pub_key: Option<np_ed25519::PublicKey<C>>,
 ) {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let section_salt: v1_salt::V1Salt<C> = [3; 16].into();
     let identity_type = EncryptedIdentityDataElementType::Private;
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::new(&key_seed);
     let key_pair = np_ed25519::KeyPair::<C>::generate();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
 
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new(
+        .section_builder(SignedEncryptedSectionEncoder::new(
             identity_type,
             section_salt,
-            &metadata_key,
-            &key_pair,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -370,7 +391,7 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&v1_header, remaining).unwrap();
+    let sections = parse_sections(v1_header, remaining).unwrap();
     assert_eq!(1, sections.len());
 
     let section = sections.into_iter().next().unwrap();
@@ -389,7 +410,9 @@
                 .unwrap_or_else(|| key_seed_hkdf.extended_signed_metadata_key_hmac_key())
                 .as_bytes(),
             expected_metadata_key_hmac: expected_metadata_key_hmac.unwrap_or_else(|| {
-                key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key)
+                key_seed_hkdf
+                    .extended_signed_metadata_key_hmac_key()
+                    .calculate_hmac(&metadata_key.0)
             }),
         });
 
@@ -400,7 +423,8 @@
     assert_eq!(
         error,
         contents
-            .try_deserialize::<C>(
+            .try_resolve_identity_and_deserialize::<C>(
+                &mut deserialization_arena!(),
                 &signed_identity_resolution_material,
                 &signed_verification_material,
             )
@@ -408,34 +432,40 @@
     );
 }
 
+#[derive(Debug, PartialEq)]
+enum DeserializeError {
+    IdentityResolutionOrDeserializationError(
+        IdentityResolutionOrDeserializationError<SignatureVerificationError>,
+    ),
+    DataElementParseError(DataElementParseError),
+}
+
 /// Run a test that mangles the advertisement contents before attempting to deserialize.
 ///
 /// Since the advertisement is ciphertext, only changes outside
-fn do_bad_deserialize_tampered<
-    A: Fn(&mut Vec<u8>),
-    S: Fn(&mut CapacityLimitedVec<u8, NP_ADV_MAX_SECTION_LEN>),
->(
-    expected_error: SignatureEncryptedSectionDeserializationError,
+fn do_bad_deserialize_tampered(
+    expected_error: DeserializeError,
     expected_section_len: Option<u8>,
-    mangle_section: S,
-    mangle_adv_contents: A,
+    mangle_section: impl Fn(&mut CapacityLimitedVec<u8, NP_ADV_MAX_SECTION_LEN>),
+    mangle_adv_contents: impl Fn(&mut Vec<u8>),
 ) {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let section_salt: v1_salt::V1Salt<CryptoProviderImpl> = [3; 16].into();
     let identity_type = EncryptedIdentityDataElementType::Private;
-    let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::new(&key_seed);
+    let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
     let key_pair = KeyPair::generate();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
 
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new(
+        .section_builder(SignedEncryptedSectionEncoder::new(
             identity_type,
             section_salt,
-            &metadata_key,
-            &key_pair,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -457,7 +487,7 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&adv_header, remaining).unwrap();
+    let sections = parse_sections(adv_header, remaining).unwrap();
     assert_eq!(1, sections.len());
 
     let section = sections.into_iter().next().unwrap();
@@ -476,23 +506,25 @@
     let section_len = 19 + 2 + 16 + 2 + 64;
     assert_eq!(
         &SignatureEncryptedSection {
-            section_header: expected_section_len.unwrap_or(section_len),
-            adv_header: &adv_header,
-            encryption_info: EncryptionInfo { bytes: encryption_info_bytes },
-            identity: EncryptedIdentityMetadata {
-                header_bytes: [0x90, 0x1],
-                offset: 1.into(),
-                identity_type: EncryptedIdentityDataElementType::Private,
+            contents: EncryptedSectionContents {
+                section_header: expected_section_len.unwrap_or(section_len),
+                adv_header,
+                encryption_info: EncryptionInfo { bytes: encryption_info_bytes },
+                identity: EncryptedIdentityMetadata {
+                    header_bytes: [0x90, 0x1],
+                    offset: 1.into(),
+                    identity_type: EncryptedIdentityDataElementType::Private,
+                },
+                all_ciphertext: &adv_mut[1 + 1 + 19 + 2..],
             },
-            all_ciphertext: &adv_mut[1 + 1 + 19 + 2..],
         },
         contents
     );
 
-    let crypto_material = MinimumFootprintV1CryptoMaterial::new::<CryptoProviderImpl>(
+    let crypto_material = V1DiscoveryCredential::new::<CryptoProviderImpl>(
         key_seed,
-        key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-        key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key),
+        key_seed_hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
+        key_seed_hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
         key_pair.public(),
     );
     let identity_resolution_material =
@@ -500,13 +532,22 @@
     let verification_material =
         crypto_material.signed_verification_material::<CryptoProviderImpl>();
 
-    assert_eq!(
-        expected_error,
-        contents
-            .try_deserialize::<CryptoProviderImpl>(
-                &identity_resolution_material,
-                &verification_material,
-            )
-            .unwrap_err()
-    );
+    match contents.try_resolve_identity_and_deserialize::<CryptoProviderImpl>(
+        &mut deserialization_arena!(),
+        &identity_resolution_material,
+        &verification_material,
+    ) {
+        Ok(section) => {
+            assert_eq!(
+                expected_error,
+                DeserializeError::DataElementParseError(
+                    section.collect_data_elements().unwrap_err()
+                ),
+            );
+        }
+        Err(e) => assert_eq!(
+            expected_error,
+            DeserializeError::IdentityResolutionOrDeserializationError(e),
+        ),
+    };
 }
diff --git a/nearby/presence/np_adv/src/extended/deserialize/mod.rs b/nearby/presence/np_adv/src/extended/deserialize/mod.rs
index 1829ad6..d431325 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/mod.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/mod.rs
@@ -18,9 +18,8 @@
 
 use alloc::vec::Vec;
 use core::array::TryFromSliceError;
-use core::borrow::Borrow;
-use core::slice;
 
+use core::fmt::Debug;
 use nom::{branch, bytes, combinator, error, multi, number, sequence};
 use strum::IntoEnumIterator as _;
 
@@ -28,23 +27,30 @@
 use crypto_provider::CryptoProvider;
 use np_hkdf::v1_salt::{self, V1Salt};
 
-use crate::extended::NP_V1_ADV_MAX_SECTION_COUNT;
+#[cfg(any(feature = "devtools", test))]
+use crate::credential::v1::V1DiscoveryCryptoMaterial;
+use crate::credential::v1::V1;
+use crate::deserialization_arena::ArenaOutOfSpace;
+#[cfg(any(feature = "devtools", test))]
+use crate::deserialization_arena::DeserializationArena;
+#[cfg(test)]
+use crate::extended::deserialize::encrypted_section::IdentityResolutionOrDeserializationError;
+use crate::extended::{NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT, NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT};
 use crate::{
-    credential::{v1::*, V1Credential},
     de_type::{EncryptedIdentityDataElementType, IdentityDataElementType},
     extended::{
         data_elements::{MIC_ENCRYPTION_SCHEME, SIGNATURE_ENCRYPTION_SCHEME},
         de_type::{DeType, ExtendedDataElementType as _},
         deserialize::encrypted_section::{
-            MicEncryptedSection, MicEncryptedSectionDeserializationError,
-            SignatureEncryptedSection, SignatureEncryptedSectionDeserializationError,
+            DeserializationError, EncryptedSectionContents, MicEncryptedSection,
+            MicVerificationError, SignatureEncryptedSection, SignatureVerificationError,
         },
-        to_array_view, DeLength, ENCRYPTION_INFO_DE_TYPE, METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN,
+        DeLength, ENCRYPTION_INFO_DE_TYPE, METADATA_KEY_LEN, NP_ADV_MAX_SECTION_LEN,
     },
-    PlaintextIdentityMode, V1Header,
+    HasIdentityMatch, MetadataKey, PlaintextIdentityMode, V1Header,
 };
 
-mod encrypted_section;
+pub(crate) mod encrypted_section;
 
 #[cfg(test)]
 mod parse_tests;
@@ -57,33 +63,39 @@
 
 /// Parse into [IntermediateSection]s, exposing the underlying parsing errors.
 /// Consumes all of `adv_body`.
-pub(crate) fn parse_sections<'a, 'h: 'a>(
-    adv_header: &'h V1Header,
-    adv_body: &'a [u8],
-) -> Result<Vec<IntermediateSection<'a>>, nom::Err<error::Error<&'a [u8]>>> {
-    combinator::all_consuming(multi::many_m_n(
-        1,
-        NP_V1_ADV_MAX_SECTION_COUNT,
-        IntermediateSection::parser_with_header(adv_header),
-    ))(adv_body)
+pub(crate) fn parse_sections(
+    adv_header: V1Header,
+    adv_body: &[u8],
+) -> Result<Vec<IntermediateSection>, nom::Err<error::Error<&[u8]>>> {
+    combinator::all_consuming(branch::alt((
+        // Public advertisement
+        multi::many_m_n(
+            1,
+            NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
+            IntermediateSection::parser_public_section(),
+        ),
+        // Encrypted advertisement
+        multi::many_m_n(
+            1,
+            NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT,
+            IntermediateSection::parser_encrypted_with_header(adv_header),
+        ),
+    )))(adv_body)
     .map(|(_rem, sections)| sections)
 }
 
 /// A partially processed section that hasn't been decrypted (if applicable) yet.
 #[derive(PartialEq, Eq, Debug)]
-// sections are large because they have a stack allocated buffer for the whole section
-#[allow(clippy::large_enum_variant)]
 pub(crate) enum IntermediateSection<'a> {
     /// A section that was not encrypted, e.g. a public identity or no-identity section.
-    Plaintext(PlaintextSection),
+    Plaintext(PlaintextSection<'a>),
     /// A section whose contents were encrypted, e.g. a private identity section.
     Ciphertext(CiphertextSection<'a>),
 }
 
 impl<'a> IntermediateSection<'a> {
-    fn parser_with_header(
-        adv_header: &'a V1Header,
-    ) -> impl Fn(&'a [u8]) -> nom::IResult<&[u8], IntermediateSection> {
+    fn parser_public_section(
+    ) -> impl Fn(&'a [u8]) -> nom::IResult<&'a [u8], IntermediateSection<'a>> {
         move |adv_body| {
             let (remaining, section_contents_len) =
                 combinator::verify(number::complete::u8, |sec_len| {
@@ -92,29 +104,42 @@
 
             // Section structure possibilities:
             // - Public Identity DE, all other DEs
-            // - No identity DE, all other DEs (including Salt, excluding identity DEs)
-            // - Encryption information, non-public Identity header, ciphertext
-            // - MIC, encryption information, non-public Identity header, ciphertext
-
             let parse_public_identity = combinator::map(
                 // 1 starting offset because of public identity before it
-                sequence::tuple((PublicIdentity::parse, parse_non_identity_des(1))),
+                sequence::tuple((PublicIdentity::parse, nom::combinator::rest)),
                 // move closure to copy section_len into scope
-                move |(_identity, des)| {
+                move |(_identity, rest)| {
                     IntermediateSection::Plaintext(PlaintextSection::new(
                         PlaintextIdentityMode::Public,
-                        SectionContents::new(/* section_header */ section_contents_len, &des),
+                        SectionContents::new(
+                            /* section_header */ section_contents_len,
+                            rest,
+                            1,
+                        ),
                     ))
                 },
             );
+            combinator::map_parser(
+                bytes::complete::take(section_contents_len),
+                // Guarantee we consume all of the section (not all of the adv body)
+                // Note: `all_consuming` should never fail, since we used the `rest` parser above.
+                combinator::all_consuming(parse_public_identity),
+            )(remaining)
+        }
+    }
 
-            // 0 starting offset because there's no identity DE
-            let parse_no_identity_de = combinator::map(parse_non_identity_des(0), move |des| {
-                IntermediateSection::Plaintext(PlaintextSection::new(
-                    PlaintextIdentityMode::None,
-                    SectionContents::new(/* section_header */ section_contents_len, &des),
-                ))
-            });
+    fn parser_encrypted_with_header(
+        adv_header: V1Header,
+    ) -> impl Fn(&'a [u8]) -> nom::IResult<&[u8], IntermediateSection> {
+        move |adv_body| {
+            let (remaining, section_contents_len) =
+                combinator::verify(number::complete::u8, |sec_len| {
+                    *sec_len as usize <= NP_ADV_MAX_SECTION_LEN && *sec_len as usize > 0
+                })(adv_body)?;
+
+            // Section structure possibilities:
+            // - Encryption information, non-public Identity header, ciphertext
+            // - Encryption information, non-public Identity header, ciphertext, MIC
 
             let parse_encrypted_identity = combinator::map(
                 sequence::tuple((
@@ -139,11 +164,13 @@
                     let to_skip = identity.header_bytes.len();
                     IntermediateSection::Ciphertext(CiphertextSection::SignatureEncryptedIdentity(
                         SignatureEncryptedSection {
-                            section_header: section_contents_len,
-                            adv_header,
-                            encryption_info,
-                            identity,
-                            all_ciphertext: &identity_and_ciphertext[to_skip..],
+                            contents: EncryptedSectionContents::new(
+                                adv_header,
+                                section_contents_len,
+                                encryption_info,
+                                identity,
+                                &identity_and_ciphertext[to_skip..],
+                            ),
                         },
                     ))
                 },
@@ -179,14 +206,16 @@
                     let to_skip = identity.header_bytes.len();
                     IntermediateSection::Ciphertext(CiphertextSection::MicEncryptedIdentity(
                         MicEncryptedSection {
-                            section_header: section_contents_len,
-                            adv_header,
+                            contents: EncryptedSectionContents::new(
+                                adv_header,
+                                section_contents_len,
+                                encryption_info,
+                                identity,
+                                &identity_and_ciphertext[to_skip..],
+                            ),
                             mic: mic.try_into().unwrap_or_else(|_| {
                                 panic!("{} is a valid length", SectionMic::CONTENTS_LEN)
                             }),
-                            encryption_info,
-                            identity,
-                            all_ciphertext: &identity_and_ciphertext[to_skip..],
                         },
                     ))
                 },
@@ -198,8 +227,6 @@
                 combinator::all_consuming(branch::alt((
                     parse_mic_encrypted_identity,
                     parse_encrypted_identity,
-                    parse_public_identity,
-                    parse_no_identity_de,
                 ))),
             )(remaining)
         }
@@ -207,61 +234,59 @@
 }
 
 #[derive(PartialEq, Eq, Debug)]
-struct SectionContents {
+struct SectionContents<'adv> {
     section_header: u8,
-    de_data: ArrayView<u8, NP_ADV_MAX_SECTION_LEN>,
-    data_elements: Vec<OffsetDataElement>,
+    de_region_excl_identity: &'adv [u8],
+    data_element_start_offset: u8,
 }
 
-impl SectionContents {
-    fn new(section_header: u8, data_elements: &[RefDataElement]) -> Self {
-        let (data_elements, de_data) = convert_data_elements(data_elements);
-
-        Self { section_header, de_data, data_elements }
+impl<'adv> SectionContents<'adv> {
+    fn new(
+        section_header: u8,
+        de_region_excl_identity: &'adv [u8],
+        data_element_start_offset: u8,
+    ) -> Self {
+        Self { section_header, de_region_excl_identity, data_element_start_offset }
     }
 
-    fn data_elements(&'_ self) -> DataElements<'_> {
-        DataElements { de_iter: self.data_elements.iter(), de_data: &self.de_data }
-    }
-}
-
-/// An iterator over data elements in a V1 section
-// A concrete type to make it easy to refer to in return types where opaque types aren't available.
-pub struct DataElements<'a> {
-    de_iter: slice::Iter<'a, OffsetDataElement>,
-    de_data: &'a ArrayView<u8, NP_ADV_MAX_SECTION_LEN>,
-}
-
-impl<'a> Iterator for DataElements<'a> {
-    type Item = DataElement<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.de_iter.next().map(|de| DataElement { de_data: self.de_data, de })
+    fn iter_data_elements(&self) -> DataElementParsingIterator<'adv> {
+        DataElementParsingIterator {
+            input: self.de_region_excl_identity,
+            offset: self.data_element_start_offset,
+        }
     }
 }
 
 /// A section deserialized from a V1 advertisement.
-pub trait Section {
+pub trait Section<'adv, E: Debug> {
     /// The iterator type used to iterate over data elements
-    type Iterator<'a>: Iterator<Item = DataElement<'a>>
-    where
-        Self: 'a;
+    type Iterator: Iterator<Item = Result<DataElement<'adv>, E>>;
 
     /// Iterator over the data elements in a section, except for any DEs related to resolving the
     /// identity or otherwise validating the payload (e.g. MIC, Signature, any identity DEs like
     /// Private Identity).
-    fn data_elements(&'_ self) -> Self::Iterator<'_>;
+    fn iter_data_elements(&self) -> Self::Iterator;
+
+    /// Collects the data elements into a vector, eagerly catching and resolving any errors during
+    /// parsing.
+    #[cfg(any(test, feature = "alloc"))]
+    fn collect_data_elements(&self) -> Result<Vec<DataElement<'adv>>, E>
+    where
+        Self: Sized,
+    {
+        self.iter_data_elements().collect()
+    }
 }
 
 /// A plaintext section deserialized from a V1 advertisement.
 #[derive(PartialEq, Eq, Debug)]
-pub struct PlaintextSection {
+pub struct PlaintextSection<'adv> {
     identity: PlaintextIdentityMode,
-    contents: SectionContents,
+    contents: SectionContents<'adv>,
 }
 
-impl PlaintextSection {
-    fn new(identity: PlaintextIdentityMode, contents: SectionContents) -> Self {
+impl<'adv> PlaintextSection<'adv> {
+    fn new(identity: PlaintextIdentityMode, contents: SectionContents<'adv>) -> Self {
         Self { identity, contents }
     }
 
@@ -274,11 +299,11 @@
     }
 }
 
-impl Section for PlaintextSection {
-    type Iterator<'a>  = DataElements<'a> where Self: 'a;
+impl<'adv> Section<'adv, DataElementParseError> for PlaintextSection<'adv> {
+    type Iterator = DataElementParsingIterator<'adv>;
 
-    fn data_elements(&'_ self) -> Self::Iterator<'_> {
-        self.contents.data_elements()
+    fn iter_data_elements(&self) -> Self::Iterator {
+        self.contents.iter_data_elements()
     }
 }
 
@@ -300,23 +325,29 @@
     }
 }
 
-/// The decrypted contents of a ciphertext section.
-#[derive(Debug, PartialEq, Eq)]
-pub struct DecryptedSection {
-    identity_type: EncryptedIdentityDataElementType,
-    verification_mode: VerificationMode,
-    metadata_key: [u8; METADATA_KEY_LEN],
-    salt: RawV1Salt,
-    contents: SectionContents,
+impl<C: CryptoProvider> From<V1Salt<C>> for RawV1Salt {
+    fn from(salt: V1Salt<C>) -> Self {
+        RawV1Salt(salt.into_array())
+    }
 }
 
-impl DecryptedSection {
+/// Fully-parsed and verified decrypted contents from an encrypted section.
+#[derive(Debug, PartialEq, Eq)]
+pub struct DecryptedSection<'adv> {
+    identity_type: EncryptedIdentityDataElementType,
+    verification_mode: VerificationMode,
+    metadata_key: MetadataKey,
+    salt: RawV1Salt,
+    contents: SectionContents<'adv>,
+}
+
+impl<'adv> DecryptedSection<'adv> {
     fn new(
         identity_type: EncryptedIdentityDataElementType,
         verification_mode: VerificationMode,
-        metadata_key: [u8; 16],
+        metadata_key: MetadataKey,
         salt: RawV1Salt,
-        contents: SectionContents,
+        contents: SectionContents<'adv>,
     ) -> Self {
         Self { identity_type, verification_mode, metadata_key, salt, contents }
     }
@@ -331,22 +362,24 @@
         self.verification_mode
     }
 
-    /// The decrypted metadata key from the identity DE.
-    pub fn metadata_key(&self) -> &[u8; 16] {
-        &self.metadata_key
-    }
-
     /// The salt used for decryption of this advertisement.
     pub fn salt(&self) -> RawV1Salt {
         self.salt
     }
 }
 
-impl Section for DecryptedSection {
-    type Iterator<'a>  = DataElements<'a> where Self: 'a;
+impl<'adv> HasIdentityMatch for DecryptedSection<'adv> {
+    type Version = V1;
+    fn metadata_key(&self) -> MetadataKey {
+        self.metadata_key
+    }
+}
 
-    fn data_elements(&'_ self) -> Self::Iterator<'_> {
-        self.contents.data_elements()
+impl<'adv> Section<'adv, DataElementParseError> for DecryptedSection<'adv> {
+    type Iterator = DataElementParsingIterator<'adv>;
+
+    fn iter_data_elements(&self) -> Self::Iterator {
+        self.contents.iter_data_elements()
     }
 }
 
@@ -357,96 +390,208 @@
 }
 
 impl<'a> CiphertextSection<'a> {
-    pub(crate) fn try_deserialize<'c, C, P>(
+    /// Tries to match this section's identity using the given crypto-material,
+    /// and if successful, tries to decrypt this section.
+    #[cfg(test)]
+    pub(crate) fn try_resolve_identity_and_deserialize<C, P>(
         &'a self,
-        credential: &'c C,
-    ) -> Result<DecryptedSection, SectionDeserializeError>
+        arena: &mut DeserializationArena<'a>,
+        crypto_material: &C,
+    ) -> Result<DecryptedSection<'a>, SectionDeserializeError>
     where
-        C: V1Credential,
+        C: V1DiscoveryCryptoMaterial,
         P: CryptoProvider,
     {
-        let crypto_material = credential.crypto_material();
         match self {
-            Self::SignatureEncryptedIdentity(contents) => {
+            CiphertextSection::SignatureEncryptedIdentity(contents) => {
                 let identity_resolution_material =
                     crypto_material.signed_identity_resolution_material::<P>();
                 let verification_material = crypto_material.signed_verification_material::<P>();
 
                 contents
-                    .try_deserialize::<P>(
-                        identity_resolution_material.borrow(),
+                    .try_resolve_identity_and_deserialize::<P>(
+                        arena,
+                        &identity_resolution_material,
                         &verification_material,
                     )
-                    .map_err(|e| match e {
-                        SignatureEncryptedSectionDeserializationError::MetadataKeyMacMismatch
-                        | SignatureEncryptedSectionDeserializationError::SignatureMismatch => {
-                            SectionDeserializeError::IncorrectCredential
-                        }
-                        SignatureEncryptedSectionDeserializationError::SignatureMissing
-                        | SignatureEncryptedSectionDeserializationError::DeParseError => {
-                            SectionDeserializeError::ParseError
-                        }
-                    })
+                    .map_err(|e| e.into())
             }
-            Self::MicEncryptedIdentity(contents) => {
+            CiphertextSection::MicEncryptedIdentity(contents) => {
                 let identity_resolution_material =
                     crypto_material.unsigned_identity_resolution_material::<P>();
                 let verification_material = crypto_material.unsigned_verification_material::<P>();
 
                 contents
-                    .try_deserialize::<P>(
-                        identity_resolution_material.borrow(),
+                    .try_resolve_identity_and_deserialize::<P>(
+                        arena,
+                        &identity_resolution_material,
                         &verification_material,
                     )
-                    .map_err(|e| match e {
-                        MicEncryptedSectionDeserializationError::MetadataKeyMacMismatch
-                        | MicEncryptedSectionDeserializationError::MicMismatch => {
-                            SectionDeserializeError::IncorrectCredential
-                        }
-                        MicEncryptedSectionDeserializationError::DeParseError => {
-                            SectionDeserializeError::ParseError
-                        }
-                    })
+                    .map_err(|e| e.into())
             }
         }
     }
+
+    /// Try decrypting into some raw bytes given some raw unsigned crypto-material.
+    #[cfg(feature = "devtools")]
+    pub(crate) fn try_resolve_identity_and_decrypt<
+        C: V1DiscoveryCryptoMaterial,
+        P: CryptoProvider,
+    >(
+        &self,
+        arena: &mut DeserializationArena<'a>,
+        crypto_material: &C,
+    ) -> Option<Result<ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, ArenaOutOfSpace>> {
+        match self {
+            CiphertextSection::SignatureEncryptedIdentity(x) => {
+                let identity_resolution_material =
+                    crypto_material.signed_identity_resolution_material::<P>();
+                x.try_resolve_identity_and_decrypt::<P>(arena, &identity_resolution_material)
+            }
+            CiphertextSection::MicEncryptedIdentity(x) => {
+                let identity_resolution_material =
+                    crypto_material.unsigned_identity_resolution_material::<P>();
+                x.try_resolve_identity_and_decrypt::<P>(arena, &identity_resolution_material)
+            }
+        }
+    }
+
+    pub(crate) fn contents(&self) -> &EncryptedSectionContents<'a> {
+        match self {
+            CiphertextSection::SignatureEncryptedIdentity(x) => &x.contents,
+            CiphertextSection::MicEncryptedIdentity(x) => &x.contents,
+        }
+    }
 }
 
-/// Errors that can occur when deserializing an advertisement
+/// Errors that can occur when deserializing a section
+/// of a V1 advertisement.
 #[derive(Debug, PartialEq, Eq)]
 pub enum SectionDeserializeError {
     /// The credential supplied did not match the section's identity data
     IncorrectCredential,
     /// Section data is malformed
     ParseError,
+    /// The given arena is not large enough to hold the decrypted contents
+    ArenaOutOfSpace,
 }
 
-/// Returns a parser function that parses data elements whose type is not one of the known identity
-/// DE types, and whose offsets start from the provided `starting_offset`.
-///
-/// Consumes all input.
-fn parse_non_identity_des(
-    starting_offset: usize,
-) -> impl Fn(&[u8]) -> nom::IResult<&[u8], Vec<RefDataElement>> {
-    move |input: &[u8]| {
-        combinator::map_opt(
-            combinator::all_consuming(multi::many0(combinator::verify(
-                ProtoDataElement::parse,
-                |de| IdentityDataElementType::iter().all(|t| t.type_code() != de.header.de_type),
-            ))),
-            |proto_des| {
-                proto_des
-                    .into_iter()
-                    .enumerate()
-                    .map(|(offset, pde)| starting_offset.checked_add(offset).map(|sum| (sum, pde)))
-                    .map(|res| {
-                        res.map(|(offset, pde)| {
-                            pde.into_data_element(v1_salt::DataElementOffset::from(offset))
-                        })
-                    })
-                    .collect::<Option<Vec<_>>>()
-            },
-        )(input)
+#[cfg(test)]
+impl From<IdentityResolutionOrDeserializationError<MicVerificationError>>
+    for SectionDeserializeError
+{
+    fn from(error: IdentityResolutionOrDeserializationError<MicVerificationError>) -> Self {
+        match error {
+            IdentityResolutionOrDeserializationError::IdentityMatchingError => {
+                Self::IncorrectCredential
+            }
+            IdentityResolutionOrDeserializationError::DeserializationError(e) => e.into(),
+        }
+    }
+}
+
+#[cfg(test)]
+impl From<IdentityResolutionOrDeserializationError<SignatureVerificationError>>
+    for SectionDeserializeError
+{
+    fn from(error: IdentityResolutionOrDeserializationError<SignatureVerificationError>) -> Self {
+        match error {
+            IdentityResolutionOrDeserializationError::IdentityMatchingError => {
+                Self::IncorrectCredential
+            }
+            IdentityResolutionOrDeserializationError::DeserializationError(e) => e.into(),
+        }
+    }
+}
+
+impl From<DeserializationError<MicVerificationError>> for SectionDeserializeError {
+    fn from(mic_deserialization_error: DeserializationError<MicVerificationError>) -> Self {
+        match mic_deserialization_error {
+            DeserializationError::VerificationError(MicVerificationError::MicMismatch) => {
+                Self::IncorrectCredential
+            }
+            DeserializationError::ArenaOutOfSpace => Self::ArenaOutOfSpace,
+        }
+    }
+}
+
+impl From<DeserializationError<SignatureVerificationError>> for SectionDeserializeError {
+    fn from(
+        signature_deserialization_error: DeserializationError<SignatureVerificationError>,
+    ) -> Self {
+        match signature_deserialization_error {
+            DeserializationError::VerificationError(
+                SignatureVerificationError::SignatureMissing,
+            ) => Self::ParseError,
+            DeserializationError::VerificationError(
+                SignatureVerificationError::SignatureMismatch,
+            ) => Self::IncorrectCredential,
+            DeserializationError::ArenaOutOfSpace => Self::ArenaOutOfSpace,
+        }
+    }
+}
+
+/// An iterator that parses the given data elements iteratively. In environments where memory is
+/// not severely constrained, it is usually safer to collect this into `Result<Vec<DataElement>>`
+/// so the validity of the whole advertisement can be checked before proceeding with further
+/// processing.
+#[derive(Debug)]
+pub struct DataElementParsingIterator<'adv> {
+    input: &'adv [u8],
+    // The index of the data element this is currently at
+    offset: u8,
+}
+
+/// The error that may arise while parsing data elements.
+#[derive(Debug, PartialEq, Eq)]
+pub enum DataElementParseError {
+    /// Only one identity data element is allowed in an advertisement, but a duplicate is found
+    /// while parsing.
+    DuplicateIdentityDataElement,
+    /// Unexpected data found after the end of the data elements portion. This means either the
+    /// parser was fed with additional data (it should only be given the bytes within a section,
+    /// not the whole advertisement), or the length field in the header of the data element is
+    /// malformed.
+    UnexpectedDataAfterEnd,
+    /// There are too many data elements in the advertisement. The maximum number supported by the
+    /// current parsing logic is 255.
+    TooManyDataElements,
+    /// A parse error is returned during nom.
+    NomError(nom::error::ErrorKind),
+}
+
+impl<'adv> Iterator for DataElementParsingIterator<'adv> {
+    type Item = Result<DataElement<'adv>, DataElementParseError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match ProtoDataElement::parse(self.input) {
+            Ok((rem, pde)) => {
+                if !IdentityDataElementType::iter().all(|t| t.type_code() != pde.header.de_type) {
+                    return Some(Err(DataElementParseError::DuplicateIdentityDataElement));
+                }
+                self.input = rem;
+                let current_offset = self.offset;
+                self.offset = if let Some(offset) = self.offset.checked_add(1) {
+                    offset
+                } else {
+                    return Some(Err(DataElementParseError::TooManyDataElements));
+                };
+                Some(Ok(pde.into_data_element(v1_salt::DataElementOffset::from(current_offset))))
+            }
+            Err(nom::Err::Failure(e)) => Some(Err(DataElementParseError::NomError(e.code))),
+            Err(nom::Err::Incomplete(_)) => {
+                panic!("Should always complete since we are parsing using the `nom::complete` APIs")
+            }
+            Err(nom::Err::Error(_)) => {
+                // nom `Error` is recoverable, it usually means we should move on the parsing the
+                // next section. There is nothing after data elements within a section, so we just
+                // check that there is no remaining data.
+                if !self.input.is_empty() {
+                    return Some(Err(DataElementParseError::UnexpectedDataAfterEnd));
+                }
+                None
+            }
+        }
     }
 }
 
@@ -533,8 +678,8 @@
 
                 type_bytes
                     .iter()
-                    .fold(Some(0_u32), |accum, b| {
-                        accum.and_then(|n| n.checked_shl(7)).map(|n| n + ((b & 0x7F) as u32))
+                    .try_fold(0_u32, |accum, b| {
+                        accum.checked_shl(7).map(|n| n + ((b & 0x7F) as u32))
                     })
                     .and_then(|type_code| {
                         ArrayView::try_from_slice(header_bytes).map(|header_bytes| DeHeader {
@@ -562,56 +707,14 @@
     fn parse(input: &[u8]) -> nom::IResult<&[u8], ProtoDataElement> {
         let (remaining, header) = DeHeader::parse(input)?;
         let len = header.contents_len;
-        combinator::map(bytes::complete::take(len.as_usize()), move |slice| {
+        combinator::map(bytes::complete::take(len.as_u8()), move |slice| {
             let header_clone = header.clone();
             ProtoDataElement { header: header_clone, contents: slice }
         })(remaining)
     }
 
-    fn into_data_element(self, offset: v1_salt::DataElementOffset) -> RefDataElement<'d> {
-        RefDataElement {
-            offset,
-            header_len: self.header.header_bytes.len().try_into().expect("header is <= 6 bytes"),
-            de_type: self.header.de_type,
-            contents: self.contents,
-        }
-    }
-}
-
-/// A data element that holds a slice reference for its contents
-#[derive(Debug, PartialEq, Eq)]
-pub(crate) struct RefDataElement<'d> {
-    pub(crate) offset: v1_salt::DataElementOffset,
-    pub(crate) header_len: u8,
-    pub(crate) de_type: DeType,
-    pub(crate) contents: &'d [u8],
-}
-
-/// A deserialized data element in a section.
-///
-/// The DE has been processed to the point of exposing a DE type and its contents as a `&[u8]`, but
-/// no DE-type-specific processing has been performed.
-#[derive(Debug)]
-pub struct DataElement<'a> {
-    de_data: &'a ArrayView<u8, NP_ADV_MAX_SECTION_LEN>,
-    de: &'a OffsetDataElement,
-}
-
-impl<'a> DataElement<'a> {
-    /// The offset of the DE in its containing Section.
-    ///
-    /// Used with the section salt to derive per-DE salt.
-    pub fn offset(&self) -> v1_salt::DataElementOffset {
-        self.de.offset
-    }
-    /// The type of the DE
-    pub fn de_type(&self) -> DeType {
-        self.de.de_type
-    }
-    /// The contents of the DE
-    pub fn contents(&self) -> &[u8] {
-        &self.de_data.as_slice()
-            [self.de.start_of_contents..self.de.start_of_contents + self.de.contents_len]
+    fn into_data_element(self, offset: v1_salt::DataElementOffset) -> DataElement<'d> {
+        DataElement { offset, de_type: self.header.de_type, contents: self.contents }
     }
 }
 
@@ -635,7 +738,7 @@
 pub struct EncryptedSectionIdentity {
     identity_type: EncryptedIdentityDataElementType,
     validation_mode: VerificationMode,
-    metadata_key: [u8; METADATA_KEY_LEN],
+    metadata_key: MetadataKey,
 }
 
 impl EncryptedSectionIdentity {
@@ -648,52 +751,37 @@
         self.validation_mode
     }
     /// The decrypted metadata key from the section's identity DE
-    pub fn metadata_key(&self) -> &[u8; METADATA_KEY_LEN] {
-        &self.metadata_key
+    pub fn metadata_key(&self) -> MetadataKey {
+        self.metadata_key
     }
 }
 
-/// A DE that designates its contents via offset and length to avoid self-referential slices.
-#[derive(Debug, PartialEq, Eq)]
-struct OffsetDataElement {
+/// A deserialized data element in a section.
+///
+/// The DE has been processed to the point of exposing a DE type and its contents as a `&[u8]`, but
+/// no DE-type-specific processing has been performed.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct DataElement<'adv> {
     offset: v1_salt::DataElementOffset,
     de_type: DeType,
-    start_of_contents: usize,
-    contents_len: usize,
+    contents: &'adv [u8],
 }
 
-/// Convert data elements from holding slices to holding offsets and lengths to avoid
-/// lifetime woes.
-/// This entails some data copying, so if it causes noticeable perf issues we can revisit
-/// it, but it is likely to be much cheaper than decryption.
-///
-/// # Panics
-///
-/// Will panic if handed more data elements than fit in one section. This is only possible if
-/// generating data elements from a source other than parsing a section.
-fn convert_data_elements(
-    elements: &[RefDataElement],
-) -> (Vec<OffsetDataElement>, ArrayView<u8, NP_ADV_MAX_SECTION_LEN>) {
-    let mut buf = tinyvec::ArrayVec::new();
-
-    (
-        elements
-            .iter()
-            .map(|de| {
-                let current_len = buf.len();
-                // won't overflow because these DEs originally came from a section, and now
-                // we're packing only their contents without DE headers
-                buf.extend_from_slice(de.contents);
-                OffsetDataElement {
-                    offset: de.offset,
-                    de_type: de.de_type,
-                    start_of_contents: current_len,
-                    contents_len: de.contents.len(),
-                }
-            })
-            .collect(),
-        to_array_view(buf),
-    )
+impl<'adv> DataElement<'adv> {
+    /// The offset of the DE in its containing Section.
+    ///
+    /// Used with the section salt to derive per-DE salt.
+    pub fn offset(&self) -> v1_salt::DataElementOffset {
+        self.offset
+    }
+    /// The type of the DE
+    pub fn de_type(&self) -> DeType {
+        self.de_type
+    }
+    /// The contents of the DE
+    pub fn contents(&self) -> &'adv [u8] {
+        self.contents
+    }
 }
 
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -787,7 +875,7 @@
         combinator::map(
             combinator::verify(DeHeader::parse, |deh| {
                 deh.de_type == IdentityDataElementType::Public.type_code()
-                    && deh.contents_len.as_usize() == 0
+                    && deh.contents_len.as_u8() == 0
             }),
             |_| PublicIdentity,
         )(input)
@@ -809,7 +897,7 @@
     // 2-byte header, 16-byte metadata key
     pub(crate) const TOTAL_DE_LEN: usize = 18;
 
-    /// Returns a parser function that parses an [EncryptedIdentity] using the provided DE `offset`.
+    /// Returns a parser function that parses an [`EncryptedIdentityMetadata`] using the provided DE `offset`.
     fn parser_at_offset(
         offset: v1_salt::DataElementOffset,
     ) -> impl Fn(&[u8]) -> nom::IResult<&[u8], EncryptedIdentityMetadata> {
diff --git a/nearby/presence/np_adv/src/extended/deserialize/parse_tests.rs b/nearby/presence/np_adv/src/extended/deserialize/parse_tests.rs
index f3b8258..506a127 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/parse_tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/parse_tests.rs
@@ -13,85 +13,17 @@
 // limitations under the License.
 
 extern crate std;
-
 use super::*;
 use crate::extended::deserialize::encrypted_section::{
     MicEncryptedSection, SignatureEncryptedSection,
 };
+use crate::extended::deserialize::test_stubs::IntermediateSectionExt;
+use rand::rngs::StdRng;
+use rand::seq::SliceRandom;
+use rand::{Rng, SeedableRng};
 use std::vec;
 
 #[test]
-fn parse_adv_ext_no_identity() {
-    // 2 sections, 2 DEs each
-    let mut adv_body = vec![];
-    // section
-    adv_body.push(9);
-    // de 1 byte header, type 5, len 5
-    adv_body.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
-    // de 2 byte header, type 6, len 1
-    adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
-
-    // section
-    adv_body.push(11);
-    // de 3 byte header, len 4
-    adv_body.extend_from_slice(&[0x84, 0xC3, 0x03, 0x01, 0x02, 0x03, 0x04]);
-    // de 1 byte header, type 8, len 3
-    adv_body.extend_from_slice(&[0x38, 0x01, 0x02, 0x03]);
-
-    assert_eq!(
-        Ok(vec![
-            PlaintextSection::new(
-                PlaintextIdentityMode::None,
-                SectionContents::new(
-                    9,
-                    &[
-                        // 1 byte header, len 5
-                        RefDataElement {
-                            offset: 0_usize.into(),
-                            header_len: 1,
-                            de_type: 5_u8.into(),
-                            contents: &[0x01, 0x02, 0x03, 0x04, 0x05]
-                        },
-                        // 2 byte header, len 1
-                        RefDataElement {
-                            offset: 1_usize.into(),
-                            header_len: 2,
-                            de_type: 6_u8.into(),
-                            contents: &[0x01]
-                        }
-                    ]
-                )
-            )
-            .into(),
-            PlaintextSection::new(
-                PlaintextIdentityMode::None,
-                SectionContents::new(
-                    11,
-                    &[
-                        // 3 byte header, len 4
-                        RefDataElement {
-                            offset: 0_usize.into(),
-                            header_len: 3,
-                            de_type: 0b0000_0000_0000_0000_0010_0001_1000_0011_u32.into(),
-                            contents: &[0x01, 0x02, 0x03, 0x04]
-                        },
-                        // 1 byte header, len 3
-                        RefDataElement {
-                            offset: 1_usize.into(),
-                            header_len: 1,
-                            de_type: 8_u8.into(),
-                            contents: &[0x01, 0x02, 0x03]
-                        }
-                    ]
-                )
-            )
-            .into()
-        ]),
-        parse_sections(&V1Header { header_byte: 0x20 }, &adv_body)
-    );
-}
-
-#[test]
 fn parse_adv_ext_public_identity() {
     // 2 sections, 3 DEs each
     let mut adv_body = vec![];
@@ -104,65 +36,28 @@
     // de 2 byte header, type 6, len 1
     adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
 
-    // section
-    adv_body.push(12);
-    // public identity
-    adv_body.push(0x03);
-    // de 3 byte header, len 4
-    adv_body.extend_from_slice(&[0x84, 0xC3, 0x03, 0x01, 0x02, 0x03, 0x04]);
-    // de 1 byte header, type 8, len 3
-    adv_body.extend_from_slice(&[0x38, 0x01, 0x02, 0x03]);
+    let parsed_sections = parse_sections(V1Header { header_byte: 0x20 }, &adv_body).unwrap();
+    assert_eq!(
+        vec![IntermediateSection::from(PlaintextSection::new(
+            PlaintextIdentityMode::Public,
+            SectionContents::new(10, &adv_body[2..], 1)
+        ))],
+        parsed_sections,
+    );
+    let expected_des = [
+        // 1 byte header, len 5
+        DataElement {
+            offset: 1_u8.into(),
+            de_type: 5_u8.into(),
+            contents: &[0x01, 0x02, 0x03, 0x04, 0x05],
+        },
+        // 2 byte header, len 1
+        DataElement { offset: 2_u8.into(), de_type: 6_u8.into(), contents: &[0x01] },
+    ];
 
     assert_eq!(
-        Ok(vec![
-            PlaintextSection::new(
-                PlaintextIdentityMode::Public,
-                SectionContents::new(
-                    10,
-                    &[
-                        // 1 byte header, len 5
-                        RefDataElement {
-                            offset: 1_usize.into(),
-                            header_len: 1,
-                            de_type: 5_u8.into(),
-                            contents: &[0x01, 0x02, 0x03, 0x04, 0x05],
-                        },
-                        // 2 byte header, len 1
-                        RefDataElement {
-                            offset: 2_usize.into(),
-                            header_len: 2,
-                            de_type: 6_u8.into(),
-                            contents: &[0x01],
-                        },
-                    ],
-                )
-            )
-            .into(),
-            PlaintextSection::new(
-                PlaintextIdentityMode::Public,
-                SectionContents::new(
-                    12,
-                    &[
-                        // 3 byte header, len 4
-                        RefDataElement {
-                            offset: 1_usize.into(),
-                            header_len: 3,
-                            de_type: 0b0000_0000_0000_0000_0010_0001_1000_0011_u32.into(),
-                            contents: &[0x01, 0x02, 0x03, 0x04],
-                        },
-                        // 1 byte header, len 3
-                        RefDataElement {
-                            offset: 2_usize.into(),
-                            header_len: 1,
-                            de_type: 8_u8.into(),
-                            contents: &[0x01, 0x02, 0x03],
-                        },
-                    ],
-                )
-            )
-            .into(),
-        ]),
-        parse_sections(&V1Header { header_byte: 0x20 }, &adv_body)
+        &expected_des[..],
+        &parsed_sections[0].as_plaintext().unwrap().collect_data_elements().unwrap()
     );
 }
 
@@ -230,48 +125,50 @@
             0x11, 0x11,
         ],
     };
-    assert_eq!(
-        Ok(vec![
-            SignatureEncryptedSection {
+    let expected_sections = [
+        SignatureEncryptedSection {
+            contents: EncryptedSectionContents {
                 section_header: 47,
-                adv_header: &adv_header,
+                adv_header,
                 encryption_info: encryption_info.clone(),
                 identity: EncryptedIdentityMetadata {
                     header_bytes: [0x90, 0x01],
-                    offset: 1_usize.into(),
+                    offset: 1_u8.into(),
                     identity_type: EncryptedIdentityDataElementType::Private,
                 },
                 // skip section header + encryption info + identity header -> end of section
                 all_ciphertext: &adv_body[1 + 19 + 2..48],
-            }
-            .into(),
-            SignatureEncryptedSection {
+            },
+        },
+        SignatureEncryptedSection {
+            contents: EncryptedSectionContents {
                 section_header: 48,
-                adv_header: &adv_header,
+                adv_header,
                 encryption_info: encryption_info.clone(),
                 identity: EncryptedIdentityMetadata {
                     header_bytes: [0x90, 0x02],
-                    offset: 1_usize.into(),
+                    offset: 1_u8.into(),
                     identity_type: EncryptedIdentityDataElementType::Trusted,
                 },
                 all_ciphertext: &adv_body[48 + 1 + 19 + 2..97],
-            }
-            .into(),
-            SignatureEncryptedSection {
+            },
+        },
+        SignatureEncryptedSection {
+            contents: EncryptedSectionContents {
                 section_header: 49,
-                adv_header: &adv_header,
+                adv_header,
                 encryption_info,
                 identity: EncryptedIdentityMetadata {
                     header_bytes: [0x90, 0x04],
-                    offset: 1_usize.into(),
+                    offset: 1_u8.into(),
                     identity_type: EncryptedIdentityDataElementType::Provisioned,
                 },
                 all_ciphertext: &adv_body[97 + 1 + 19 + 2..],
-            }
-            .into()
-        ]),
-        parse_sections(&adv_header, &adv_body)
-    );
+            },
+        },
+    ];
+    let parsed_sections = parse_sections(adv_header, &adv_body).unwrap();
+    assert_eq!(parsed_sections, &expected_sections.map(IntermediateSection::from));
 }
 
 #[test]
@@ -353,145 +250,53 @@
             0x11, 0x11,
         ],
     };
-    assert_eq!(
-        Ok(vec![
-            MicEncryptedSection {
+    let expected_sections = [
+        MicEncryptedSection {
+            contents: EncryptedSectionContents {
                 section_header: 63,
-                adv_header: &adv_header,
-                mic: SectionMic::from([0x33; 16]),
+                adv_header,
                 encryption_info: encryption_info.clone(),
                 identity: EncryptedIdentityMetadata {
                     header_bytes: [0x90, 0x01],
-                    offset: 1_usize.into(),
+                    offset: 1_u8.into(),
                     identity_type: EncryptedIdentityDataElementType::Private,
                 },
                 // skip section header +  encryption info + identity header -> end of ciphertext
                 all_ciphertext: &adv_body[1 + 19 + 2..64 - 16],
-            }
-            .into(),
-            MicEncryptedSection {
+            },
+            mic: SectionMic::from([0x33; 16]),
+        },
+        MicEncryptedSection {
+            contents: EncryptedSectionContents {
                 section_header: 64,
-                adv_header: &adv_header,
-                mic: SectionMic::from([0x66; 16]),
+                adv_header,
                 encryption_info: encryption_info.clone(),
                 identity: EncryptedIdentityMetadata {
                     header_bytes: [0x90, 0x02],
-                    offset: 1_usize.into(),
+                    offset: 1_u8.into(),
                     identity_type: EncryptedIdentityDataElementType::Trusted,
                 },
                 all_ciphertext: &adv_body[64 + 1 + 19 + 2..129 - 16],
-            }
-            .into(),
-            MicEncryptedSection {
+            },
+            mic: SectionMic::from([0x66; 16]),
+        },
+        MicEncryptedSection {
+            contents: EncryptedSectionContents {
                 section_header: 65,
-                adv_header: &adv_header,
-                mic: SectionMic::from([0x99; 16]),
+                adv_header,
                 encryption_info,
                 identity: EncryptedIdentityMetadata {
                     header_bytes: [0x90, 0x04],
-                    offset: 1_usize.into(),
+                    offset: 1_u8.into(),
                     identity_type: EncryptedIdentityDataElementType::Provisioned,
                 },
                 all_ciphertext: &adv_body[129 + 1 + 19 + 2..195 - 16],
-            }
-            .into(),
-        ]),
-        parse_sections(&adv_header, &adv_body)
-    );
-}
-
-#[test]
-fn public_identity_not_first_de_error() {
-    let mut adv_body = vec![];
-
-    // section
-    adv_body.push(3 + 1);
-    // misc other DE
-    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
-    // public identity after another DE
-    adv_body.push(0x03);
-
-    assert_eq!(
-        Err(nom::Err::Error(error::Error {
-            input: &adv_body[4..],
-            // Eof because all_consuming is used to ensure complete section is parsed
-            code: error::ErrorKind::Eof
-        })),
-        parse_sections(&V1Header { header_byte: 0x20 }, &adv_body)
-    );
-}
-
-#[test]
-fn public_identity_after_public_identity_error() {
-    let mut adv_body = vec![];
-
-    // section
-    adv_body.push(1 + 3 + 1);
-    // public identity after another DE
-    adv_body.push(0x03);
-    // misc other DE
-    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
-    // public identity after another DE
-    adv_body.push(0x03);
-
-    assert_eq!(
-        Err(nom::Err::Error(error::Error {
-            // since we use many1, the parser consumes the first byte and returns the remaining in the error
-            input: &adv_body[1..],
-            // Eof because all_consuming is used to ensure complete section is parsed
-            code: error::ErrorKind::Eof
-        })),
-        parse_sections(&V1Header { header_byte: 0x20 }, &adv_body)
-    );
-}
-
-#[test]
-fn salt_public_identity_error() {
-    let mut adv_body = vec![];
-    // section
-    adv_body.push(3 + 1 + 3);
-    // salt - 1 + 2x 0x22 (invalid: must be first DE)
-    adv_body.extend_from_slice(&[0x20, 0x22, 0x22]);
-    // public identity
-    adv_body.push(0x03);
-    // misc other DE
-    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
-
-    assert_eq!(
-        Err(nom::Err::Error(error::Error {
-            input: &adv_body[4..],
-            // Eof because all_consuming is used to ensure complete section is parsed
-            code: error::ErrorKind::Eof
-        })),
-        parse_sections(&V1Header { header_byte: 0x20 }, &adv_body)
-    );
-}
-
-#[test]
-fn salt_mic_public_identity_error() {
-    let mut adv_body = vec![];
-    // section
-    adv_body.push(3 + 18 + 1 + 3);
-    // salt - 1 + 2x 0x22 (invalid: must be first DE)
-    adv_body.extend_from_slice(&[0x20, 0x22, 0x22]);
-    // mic - 2 + 16x 0x33
-    adv_body.extend_from_slice(&[
-        0x90, 0x13, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
-        0x33, 0x33, 0x33,
-    ]);
-    // public identity
-    adv_body.push(0x03);
-    // misc other DE
-    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
-
-    assert_eq!(
-        Err(nom::Err::Error(error::Error {
-            input: &adv_body[22..],
-            // Eof because all_consuming is used to ensure complete section is parsed
-            code: error::ErrorKind::Eof
-        })),
-        parse_sections(&V1Header { header_byte: 0x20 }, &adv_body)
-    );
+            },
+            mic: SectionMic::from([0x99; 16]),
+        },
+    ];
+    let parsed_sections = parse_sections(adv_header, &adv_body).unwrap();
+    assert_eq!(parsed_sections, &expected_sections.map(IntermediateSection::from));
 }
 
 #[test]
@@ -616,6 +421,191 @@
 }
 
 #[test]
+fn public_identity_not_first_de_error() {
+    let mut adv_body = vec![];
+    // section
+    adv_body.push(3 + 1);
+    // misc other DE
+    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
+    // public identity after another DE
+    adv_body.push(0x03);
+
+    assert_eq!(
+        nom::Err::Error(error::Error { input: &adv_body[1..], code: error::ErrorKind::Verify }),
+        parse_sections(V1Header { header_byte: 0x20 }, &adv_body).unwrap_err()
+    );
+}
+
+#[test]
+fn invalid_public_section() {
+    let mut rng = StdRng::from_entropy();
+    for _ in 0..100 {
+        let mut adv_body = vec![];
+        // Add section length
+        let remove_section_len = rng.gen_bool(0.5);
+        // Add public identity
+        let add_public_identity = rng.gen_bool(0.5);
+        // Add DEs
+        let add_des = rng.gen_bool(0.5);
+        // Shuffle adv
+        let shuffle = rng.gen_bool(0.5);
+
+        adv_body.push(0);
+        if add_public_identity {
+            adv_body[0] += 1;
+            adv_body.push(0x03);
+        }
+        if add_des {
+            adv_body[0] += 1;
+            adv_body.extend_from_slice(&[0x81]);
+        }
+        if remove_section_len {
+            adv_body.remove(0);
+        }
+
+        let ordered_adv = adv_body.clone();
+
+        if shuffle {
+            adv_body.shuffle(&mut rng);
+        }
+        // A V1 public section is invalid if
+        // * section length is missing
+        // * the section is empty
+        // * the section identity is missing
+        // * the identity does not follow the section length
+        // * the section length is 0
+        if remove_section_len || !add_public_identity || (ordered_adv != adv_body) {
+            parse_sections(V1Header { header_byte: 0x20 }, &adv_body)
+                .expect_err("Expected to fail because of missing section length or identity");
+        }
+    }
+}
+
+// There can only be one identity DE
+#[test]
+fn public_identity_after_public_identity_error() {
+    let mut adv_body = vec![];
+    // section
+    adv_body.push(1 + 3 + 1);
+    // public identity after another DE
+    adv_body.push(0x03);
+    // misc other DE
+    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
+    // public identity after another DE
+    adv_body.push(0x03);
+
+    let sections = parse_sections(V1Header { header_byte: 0x20 }, &adv_body).unwrap();
+    assert_eq!(sections.len(), 1);
+    assert_eq!(
+        DataElementParseError::DuplicateIdentityDataElement,
+        sections[0].as_plaintext().unwrap().collect_data_elements().unwrap_err()
+    );
+}
+
+#[test]
+fn salt_public_identity_error() {
+    let mut adv_body = vec![];
+    // section
+    adv_body.push(3 + 1 + 3);
+    // salt - 1 + 2x 0x22 (invalid: must be first DE)
+    adv_body.extend_from_slice(&[0x20, 0x22, 0x22]);
+    // public identity
+    adv_body.push(0x03);
+    // misc other DE
+    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
+
+    assert_eq!(
+        nom::Err::Error(error::Error {
+            input: &adv_body[1..],
+            // Eof because all_consuming is used to ensure complete section is parsed
+            code: error::ErrorKind::Verify
+        }),
+        parse_sections(V1Header { header_byte: 0x20 }, &adv_body).unwrap_err()
+    );
+}
+
+#[test]
+fn salt_mic_public_identity_error() {
+    let mut adv_body = vec![];
+    // section
+    adv_body.push(3 + 18 + 1 + 3);
+    // salt - 1 + 2x 0x22 (invalid: must be first DE)
+    adv_body.extend_from_slice(&[0x20, 0x22, 0x22]);
+    // mic - 2 + 16x 0x33
+    adv_body.extend_from_slice(&[
+        0x90, 0x13, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+        0x33, 0x33, 0x33,
+    ]);
+    // public identity
+    adv_body.push(0x03);
+    // misc other DE
+    adv_body.extend_from_slice(&[0x81, 0x70, 0xFF]);
+
+    assert_eq!(
+        nom::Err::Error(error::Error {
+            input: &adv_body[1..],
+            // Eof because all_consuming is used to ensure complete section is parsed
+            code: error::ErrorKind::Verify
+        }),
+        parse_sections(V1Header { header_byte: 0x20 }, &adv_body).unwrap_err()
+    );
+}
+
+#[test]
+fn parse_adv_no_identity() {
+    let adv_body = vec![0x55, 0x01, 0x02, 0x03, 0x04, 0x05];
+    assert_eq!(
+        nom::Err::Error(error::Error { input: &adv_body[1..], code: error::ErrorKind::Eof }),
+        parse_sections(V1Header { header_byte: 0x20 }, &adv_body).unwrap_err()
+    );
+}
+
+#[test]
+fn parse_empty_section() {
+    // empty section - should return an EOF error
+    let input = [];
+    assert_eq!(
+        nom::Err::Error(error::Error {
+            // attempted to read section contents
+            input: input.as_slice(),
+            code: error::ErrorKind::Eof
+        }),
+        IntermediateSection::parser_encrypted_with_header(V1Header { header_byte: 0x20 })(&input)
+            .unwrap_err()
+    );
+}
+
+#[test]
+fn parse_de_header_non_canonical_multi_byte() {
+    // length 1, type 1
+    // first byte of type doesn't have any bits in it so it contributes nothing
+    let input = [0b1000_0001, 0b1000_0000, 0b0000_0001];
+    assert_eq!(
+        nom::Err::Error(error::Error {
+            // attempted to read first type byte
+            input: &input.as_slice()[1..],
+            code: error::ErrorKind::Verify
+        }),
+        DeHeader::parse(&input).unwrap_err()
+    );
+}
+
+#[test]
+fn parse_section_length_zero() {
+    // Section length of 0 - should return a verification error
+    let input = [0x00];
+    assert_eq!(
+        nom::Err::Error(error::Error {
+            // attempted to read section contents
+            input: input.as_slice(),
+            code: error::ErrorKind::Verify
+        }),
+        IntermediateSection::parser_encrypted_with_header(V1Header { header_byte: 0x20 })(&input)
+            .unwrap_err()
+    );
+}
+
+#[test]
 fn parse_section_length_overrun() {
     // section of length 0xF0 - legal but way longer than 3
     let input = [0xF0, 0x01, 0x02, 0x03];
@@ -625,7 +615,7 @@
             input: &input.as_slice()[1..],
             code: error::ErrorKind::Eof
         }),
-        IntermediateSection::parser_with_header(&V1Header { header_byte: 0x20 })(&input)
+        IntermediateSection::parser_encrypted_with_header(V1Header { header_byte: 0x20 })(&input)
             .unwrap_err()
     );
 }
@@ -659,47 +649,33 @@
 }
 
 #[test]
-fn parse_de_header_non_canonical_multi_byte() {
-    // length 1, type 1
-    // first byte of type doesn't have any bits in it so it contributes nothing
-    let input = [0b1000_0001, 0b1000_0000, 0b0000_0001];
-    assert_eq!(
-        nom::Err::Error(error::Error {
-            // attempted to read first type byte
-            input: &input.as_slice()[1..],
-            code: error::ErrorKind::Verify
-        }),
-        DeHeader::parse(&input).unwrap_err()
-    );
-}
+fn parse_adv_signature_encrypted_plaintext_mix() {
+    // 2 sections
+    let mut adv_body = vec![];
 
-#[test]
-fn parse_section_length_zero() {
-    // Section length of 0 - should return a verification error
-    let input = [0x00];
-    assert_eq!(
-        nom::Err::Error(error::Error {
-            // attempted to read section contents
-            input: input.as_slice(),
-            code: error::ErrorKind::Verify
-        }),
-        IntermediateSection::parser_with_header(&V1Header { header_byte: 0x20 })(&input)
-            .unwrap_err()
-    );
-}
+    // section 1 - plaintext - 10 bytes
+    adv_body.push(10);
+    // public identity
+    adv_body.push(0x03);
+    // de 1 byte header, type 5, len 5
+    adv_body.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
+    // de 2 byte header, type 6, len 1
+    adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
 
-#[test]
-fn parse_empty_section() {
-    // empty section - should return an EOF error
-    let input = [];
+    // section 2 - plaintext - 10 bytes
+    adv_body.push(10);
+    // public identity
+    adv_body.push(0x03);
+    // de 1 byte header, type 5, len 5
+    adv_body.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
+    // de 2 byte header, type 6, len 1
+    adv_body.extend_from_slice(&[0x81, 0x06, 0x01]);
+
+    let adv_header = V1Header { header_byte: 0x20 };
+
     assert_eq!(
-        nom::Err::Error(error::Error {
-            // attempted to read section contents
-            input: input.as_slice(),
-            code: error::ErrorKind::Eof
-        }),
-        IntermediateSection::parser_with_header(&V1Header { header_byte: 0x20 })(&input)
-            .unwrap_err()
+        nom::Err::Error(error::Error { input: &adv_body[11..], code: error::ErrorKind::Eof }),
+        parse_sections(adv_header, &adv_body).unwrap_err()
     );
 }
 
@@ -717,8 +693,8 @@
     }
 }
 
-impl<'a> From<PlaintextSection> for IntermediateSection<'a> {
-    fn from(s: PlaintextSection) -> Self {
+impl<'a> From<PlaintextSection<'a>> for IntermediateSection<'a> {
+    fn from(s: PlaintextSection<'a>) -> Self {
         IntermediateSection::Plaintext(s)
     }
 }
diff --git a/nearby/presence/np_adv/src/extended/deserialize/section_tests.rs b/nearby/presence/np_adv/src/extended/deserialize/section_tests.rs
index 7199031..39644ab 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/section_tests.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/section_tests.rs
@@ -15,26 +15,29 @@
 extern crate std;
 
 use super::*;
+use crate::deserialization_arena;
+use crate::extended::serialize::AdvertisementType;
+use crate::extended::NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT;
 use crate::{
     credential::{
-        simple::SimpleV1Credential,
-        source::{CredentialSource, SliceCredentialSource},
-        MatchableCredential, V1Credential,
+        source::{DiscoveryCredentialSource, SliceCredentialSource},
+        v1::{SignedBroadcastCryptoMaterial, SimpleSignedBroadcastCryptoMaterial, V1},
+        DiscoveryCryptoMaterial, EmptyMatchedCredential, MatchableCredential,
+        MetadataMatchedCredential, SimpleBroadcastCryptoMaterial,
     },
     extended::{
         data_elements::GenericDataElement,
-        deserialize::{
-            convert_data_elements,
-            test_stubs::{HkdfCryptoMaterial, IntermediateSectionExt},
-            OffsetDataElement,
-        },
+        deserialize::{test_stubs::IntermediateSectionExt, DataElement},
         serialize::{
-            self, AdvBuilder, MicEncrypted, SectionBuilder, SignedEncrypted, WriteDataElement,
+            self, AdvBuilder, MicEncryptedSectionEncoder, PublicSectionEncoder, SectionBuilder,
+            SignedEncryptedSectionEncoder, WriteDataElement,
         },
         MAX_DE_LEN,
     },
-    parse_adv_header, AdvHeader, PublicIdentity,
+    parse_adv_header, AdvHeader, WithMatchedCredential,
 };
+use core::borrow::{Borrow, BorrowMut};
+use core::convert::Into;
 use crypto_provider::{CryptoProvider, CryptoRng};
 use crypto_provider_default::CryptoProviderImpl;
 use rand::{seq::SliceRandom as _, Rng as _, SeedableRng as _};
@@ -45,11 +48,10 @@
 
 #[test]
 fn deserialize_public_identity_section() {
-    do_deserialize_section_unencrypted::<PublicIdentity>(
-        PublicIdentity::default(),
+    do_deserialize_section_unencrypted::<PublicSectionEncoder>(
+        PublicSectionEncoder::default(),
         PlaintextIdentityMode::Public,
         1,
-        1,
     );
 }
 
@@ -68,36 +70,52 @@
         // share a metadata key to emphasize that we're _only_ using the identity to
         // differentiate
         let metadata_key: [u8; 16] = rng.gen();
+        let metadata_key = MetadataKey(metadata_key);
 
         let creds = identities
             .iter()
-            .enumerate()
-            .map(|(index, (key_seed, key_pair))| {
-                SimpleV1Credential::new(
-                    HkdfCryptoMaterial::new(key_seed, &metadata_key, key_pair.public()),
-                    index,
+            .map(|(key_seed, key_pair)| {
+                SimpleSignedBroadcastCryptoMaterial::new(
+                    *key_seed,
+                    metadata_key,
+                    key_pair.private_key(),
                 )
             })
+            .enumerate()
+            .map(|(index, broadcast_cm)| {
+                let match_data = MetadataMatchedCredential::<Vec<u8>>::encrypt_from_plaintext::<
+                    _,
+                    _,
+                    CryptoProviderImpl,
+                >(&broadcast_cm, &[index as u8]);
+
+                let discovery_credential =
+                    broadcast_cm.derive_v1_discovery_credential::<CryptoProviderImpl>();
+
+                MatchableCredential { discovery_credential, match_data }
+            })
             .collect::<Vec<_>>();
+
         let cred_source = SliceCredentialSource::new(&creds);
 
         let identity_type =
             *EncryptedIdentityDataElementType::iter().collect::<Vec<_>>().choose(&mut rng).unwrap();
 
-        let mut adv_builder = AdvBuilder::new();
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
-        let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(chosen_key_seed);
+        let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(*chosen_key_seed, metadata_key);
+
         let mut section_builder = adv_builder
-            .section_builder(MicEncrypted::new_random_salt(
+            .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                 &mut crypto_rng,
                 identity_type,
-                &metadata_key,
-                &hkdf,
+                &broadcast_cm,
             ))
             .unwrap();
 
-        let (expected_de_data, expected_des, orig_des) =
-            fill_section_random_des(&mut rng, &mut section_builder, 2);
+        let mut expected_de_data = vec![];
+        let (expected_des, orig_des) =
+            fill_section_random_des(&mut rng, &mut expected_de_data, &mut section_builder, 2);
 
         section_builder.add_to_advertisement();
 
@@ -111,31 +129,35 @@
             panic!("incorrect header");
         };
 
-        let sections = parse_sections(&v1_header, remaining).unwrap();
+        let sections = parse_sections(v1_header, remaining).unwrap();
         assert_eq!(1, sections.len());
-        let (section, cred) = try_deserialize_all_creds::<_, _, CryptoProviderImpl>(
-            sections[0].as_ciphertext().unwrap(),
-            &cred_source,
-        )
-        .unwrap()
-        .unwrap();
 
-        assert_eq!(&chosen_index, cred.matched_data());
+        let arena = deserialization_arena!();
 
+        let section = sections[0].as_ciphertext().unwrap();
+        let matched_section =
+            try_deserialize_all_creds::<_, CryptoProviderImpl>(arena, section, &cred_source)
+                .unwrap()
+                .unwrap();
+
+        // Verify that the decrypted metadata contains the chosen index
+        let decrypted_metadata = matched_section.decrypt_metadata::<CryptoProviderImpl>().unwrap();
+        assert_eq!(&[chosen_index as u8] as &[u8], &decrypted_metadata);
+
+        // Verify that the section contents passed through unaltered
+        let section = matched_section.contents();
         assert_eq!(section.identity_type(), identity_type);
         assert_eq!(section.verification_mode(), VerificationMode::Mic);
-        assert_eq!(section.metadata_key(), &metadata_key);
+        assert_eq!(section.metadata_key(), metadata_key);
         assert_eq!(
-            section.contents,
-            SectionContents {
-                section_header: (19 + 2 + 16 + total_de_len(&orig_des) + 16) as u8,
-                de_data: ArrayView::try_from_slice(expected_de_data.as_slice()).unwrap(),
-                data_elements: expected_des,
-            }
+            section.contents.section_header,
+            (19 + 2 + 16 + total_de_len(&orig_des) + 16) as u8
         );
+        let data_elements = section.collect_data_elements().unwrap();
+        assert_eq!(data_elements, expected_des);
         assert_eq!(
-            section
-                .data_elements()
+            data_elements
+                .iter()
                 .map(|de| GenericDataElement::try_from(de.de_type(), de.contents()).unwrap())
                 .collect::<Vec<_>>(),
             orig_des
@@ -156,47 +178,55 @@
         // share a metadata key to emphasize that we're _only_ using the identity to
         // differentiate
         let metadata_key: [u8; 16] = rng.gen();
+        let metadata_key = MetadataKey(metadata_key);
 
         let creds = identities
             .iter()
-            .enumerate()
-            .map(|(index, (key_seed, key_pair))| {
-                let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(key_seed);
-                let unsigned =
-                    hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(&metadata_key);
-                let signed =
-                    hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key);
-                SimpleV1Credential::new(
-                    HkdfCryptoMaterial {
-                        hkdf: *key_seed,
-                        expected_unsigned_metadata_key_hmac: unsigned,
-                        expected_signed_metadata_key_hmac: signed,
-                        pub_key: key_pair.public().to_bytes(),
-                    },
-                    index,
+            .map(|(key_seed, key_pair)| {
+                SimpleSignedBroadcastCryptoMaterial::new(
+                    *key_seed,
+                    metadata_key,
+                    key_pair.private_key(),
                 )
             })
+            .enumerate()
+            .map(|(index, broadcast_cm)| {
+                let match_data = MetadataMatchedCredential::<Vec<u8>>::encrypt_from_plaintext::<
+                    _,
+                    _,
+                    CryptoProviderImpl,
+                >(&broadcast_cm, &[index as u8]);
+
+                let discovery_credential =
+                    broadcast_cm.derive_v1_discovery_credential::<CryptoProviderImpl>();
+                MatchableCredential { discovery_credential, match_data }
+            })
             .collect::<Vec<_>>();
+
         let cred_source = SliceCredentialSource::new(&creds);
 
         let identity_type =
             *EncryptedIdentityDataElementType::iter().collect::<Vec<_>>().choose(&mut rng).unwrap();
 
-        let mut adv_builder = AdvBuilder::new();
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
-        let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(chosen_key_seed);
+        let broadcast_cm = SimpleSignedBroadcastCryptoMaterial::new(
+            *chosen_key_seed,
+            metadata_key,
+            chosen_key_pair.private_key(),
+        );
+
         let mut section_builder = adv_builder
-            .section_builder(SignedEncrypted::new_random_salt(
+            .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                 &mut crypto_rng,
                 identity_type,
-                &metadata_key,
-                chosen_key_pair,
-                &hkdf,
+                &broadcast_cm,
             ))
             .unwrap();
 
-        let (expected_de_data, expected_des, orig_des) =
-            fill_section_random_des(&mut rng, &mut section_builder, 2);
+        let mut expected_de_data = vec![];
+        let (expected_des, orig_des) =
+            fill_section_random_des(&mut rng, &mut expected_de_data, &mut section_builder, 2);
 
         section_builder.add_to_advertisement();
 
@@ -210,31 +240,35 @@
             panic!("incorrect header");
         };
 
-        let sections = parse_sections(&v1_header, remaining).unwrap();
+        let arena = deserialization_arena!();
+
+        let sections = parse_sections(v1_header, remaining).unwrap();
         assert_eq!(1, sections.len());
-        let (section, cred) = try_deserialize_all_creds::<_, _, CryptoProviderImpl>(
-            sections[0].as_ciphertext().unwrap(),
-            &cred_source,
-        )
-        .unwrap()
-        .unwrap();
 
-        assert_eq!(&chosen_index, cred.matched_data());
+        let section = sections[0].as_ciphertext().unwrap();
+        let matched_section =
+            try_deserialize_all_creds::<_, CryptoProviderImpl>(arena, section, &cred_source)
+                .unwrap()
+                .unwrap();
 
+        // Verify that the decrypted metadata contains the chosen index
+        let decrypted_metadata = matched_section.decrypt_metadata::<CryptoProviderImpl>().unwrap();
+        assert_eq!(&[chosen_index as u8] as &[u8], &decrypted_metadata);
+
+        // Verify that the section contents passed through unaltered
+        let section = matched_section.contents();
         assert_eq!(section.identity_type(), identity_type);
         assert_eq!(section.verification_mode(), VerificationMode::Signature);
-        assert_eq!(section.metadata_key(), &metadata_key);
+        assert_eq!(section.metadata_key(), metadata_key);
         assert_eq!(
-            section.contents,
-            SectionContents {
-                section_header: (19 + 2 + 16 + 64 + total_de_len(&orig_des)) as u8,
-                de_data: ArrayView::try_from_slice(expected_de_data.as_slice()).unwrap(),
-                data_elements: expected_des,
-            }
+            section.contents.section_header,
+            (19 + 2 + 16 + 64 + total_de_len(&orig_des)) as u8
         );
+        let data_elements = section.collect_data_elements().unwrap();
+        assert_eq!(data_elements, expected_des);
         assert_eq!(
-            section
-                .data_elements()
+            data_elements
+                .iter()
                 .map(|de| GenericDataElement::try_from(de.de_type(), de.contents()).unwrap())
                 .collect::<Vec<_>>(),
             orig_des
@@ -257,44 +291,60 @@
         // share a metadata key to emphasize that we're _only_ using the identity to
         // differentiate
         let metadata_key: [u8; 16] = rng.gen();
+        let metadata_key = MetadataKey(metadata_key);
 
         let credentials = identities
             .iter()
-            .enumerate()
-            .map(|(index, (key_seed, key_pair))| {
-                SimpleV1Credential::new(
-                    HkdfCryptoMaterial::new(key_seed, &metadata_key, key_pair.public()),
-                    index,
+            .map(|(key_seed, key_pair)| {
+                SimpleSignedBroadcastCryptoMaterial::new(
+                    *key_seed,
+                    metadata_key,
+                    key_pair.private_key(),
                 )
+                .derive_v1_discovery_credential::<CryptoProviderImpl>()
+            })
+            .map(|discovery_credential| MatchableCredential {
+                discovery_credential,
+                match_data: EmptyMatchedCredential,
             })
             .collect::<Vec<_>>();
+
         let cred_source = SliceCredentialSource::new(&credentials);
 
         let identity_type =
             *EncryptedIdentityDataElementType::iter().collect::<Vec<_>>().choose(&mut rng).unwrap();
 
-        let mut adv_builder = AdvBuilder::new();
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
-        let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&chosen_key_seed);
+        let broadcast_cm = SimpleSignedBroadcastCryptoMaterial::new(
+            chosen_key_seed,
+            metadata_key,
+            chosen_key_pair.private_key(),
+        );
 
-        // awkward split because SectionIdentity isn't object-safe, so we can't just have a
-        // Box<dyn SectionIdentity> and use that in one code path
+        // awkward split because SectionEncoder isn't object-safe, so we can't just have a
+        // Box<dyn SectionEncoder> and use that in one code path
         if signed {
-            let identity = SignedEncrypted::new_random_salt(
+            let identity = SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                 &mut crypto_rng,
                 identity_type,
-                &metadata_key,
-                &chosen_key_pair,
-                &hkdf,
+                &broadcast_cm,
             );
             let mut section_builder = adv_builder.section_builder(identity).unwrap();
-            let _ = fill_section_random_des(&mut rng, &mut section_builder, 2);
+            let mut expected_de_data = vec![];
+            let _ =
+                fill_section_random_des(&mut rng, &mut expected_de_data, &mut section_builder, 2);
             section_builder.add_to_advertisement();
         } else {
-            let identity =
-                MicEncrypted::new_random_salt(&mut crypto_rng, identity_type, &metadata_key, &hkdf);
+            let identity = MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
+                &mut crypto_rng,
+                identity_type,
+                &broadcast_cm,
+            );
             let mut section_builder = adv_builder.section_builder(identity).unwrap();
-            let _ = fill_section_random_des(&mut rng, &mut section_builder, 2);
+            let mut expected_de_data = vec![];
+            let _ =
+                fill_section_random_des(&mut rng, &mut expected_de_data, &mut section_builder, 2);
             section_builder.add_to_advertisement();
         };
 
@@ -306,12 +356,13 @@
             panic!("incorrect header");
         };
 
-        let sections = parse_sections(&v1_header, remaining).unwrap();
+        let sections = parse_sections(v1_header, remaining).unwrap();
         assert_eq!(1, sections.len());
 
-        assert!(try_deserialize_all_creds::<_, _, CryptoProviderImpl>(
+        assert!(try_deserialize_all_creds::<_, CryptoProviderImpl>(
+            deserialization_arena!(),
             sections[0].as_ciphertext().unwrap(),
-            &cred_source
+            &cred_source,
         )
         .unwrap()
         .is_none());
@@ -319,138 +370,30 @@
 }
 
 #[test]
-fn convert_data_elements_empty() {
-    let orig_des = vec![];
-
-    let (des, data) = convert_data_elements(&orig_des);
-
-    assert_eq!(Vec::<OffsetDataElement>::new(), des);
-    assert_eq!(&Vec::<u8>::new(), data.as_slice());
-}
-
-#[test]
-fn convert_data_elements_just_fits() {
-    // this is actually longer than any real section's worth of DEs could be since we aren't putting
-    // DE headers in the array
-    let orig_data = vec![0x33; 1000];
-
-    let orig_des = vec![
-        RefDataElement {
-            offset: 2.into(),
-            header_len: 2,
-            de_type: 100_u32.into(),
-            contents: &orig_data[0..10],
-        },
-        RefDataElement {
-            offset: 3.into(),
-            header_len: 2,
-            de_type: 101_u32.into(),
-            contents: &orig_data[10..100],
-        },
-        RefDataElement {
-            offset: 4.into(),
-            header_len: 2,
-            de_type: 102_u32.into(),
-            contents: &orig_data[100..NP_ADV_MAX_SECTION_LEN],
-        },
-    ];
-
-    let (des, data) = convert_data_elements(&orig_des);
-
-    assert_eq!(
-        &[
-            OffsetDataElement {
-                offset: 2.into(),
-                de_type: 100_u32.into(),
-                start_of_contents: 0,
-                contents_len: 10
-            },
-            OffsetDataElement {
-                offset: 3.into(),
-                de_type: 101_u32.into(),
-                start_of_contents: 10,
-                contents_len: 90
-            },
-            OffsetDataElement {
-                offset: 4.into(),
-                de_type: 102_u32.into(),
-                start_of_contents: 100,
-                contents_len: NP_ADV_MAX_SECTION_LEN - 100,
-            },
-        ],
-        &des[..]
-    );
-    assert_eq!(&[0x33; NP_ADV_MAX_SECTION_LEN], data.as_slice());
-}
-
-#[test]
-#[should_panic]
-fn convert_data_elements_doesnt_fit_panic() {
-    let orig_data = vec![0x33; 1000];
-    let orig_des = vec![
-        RefDataElement {
-            offset: 2.into(),
-            header_len: 2,
-            de_type: 100_u32.into(),
-            contents: &orig_data[0..10],
-        },
-        // impossibly large DE
-        RefDataElement {
-            offset: 3.into(),
-            header_len: 2,
-            de_type: 101_u32.into(),
-            contents: &orig_data[10..500],
-        },
-    ];
-
-    let _ = convert_data_elements(&orig_des);
-}
-
-#[test]
 fn section_des_expose_correct_data() {
-    let mut orig_data = Vec::new();
-    orig_data.resize(130, 0);
-    for (index, byte) in orig_data.iter_mut().enumerate() {
-        *byte = index as u8;
-    }
-
-    let orig_des = vec![
-        OffsetDataElement {
-            offset: 2.into(),
-            de_type: 100_u32.into(),
-            start_of_contents: 0,
-            contents_len: 10,
-        },
-        OffsetDataElement {
-            offset: 3.into(),
-            de_type: 101_u32.into(),
-            start_of_contents: 10,
-            contents_len: 90,
-        },
-        OffsetDataElement {
-            offset: 4.into(),
-            de_type: 102_u32.into(),
-            start_of_contents: 100,
-            contents_len: 30,
-        },
-    ];
+    // 2 sections, 3 DEs each
+    let mut de_data = vec![];
+    // de 1 byte header, type 5, len 5
+    de_data.extend_from_slice(&[0x55, 0x01, 0x02, 0x03, 0x04, 0x05]);
+    // de 2 byte header, type 16, len 1
+    de_data.extend_from_slice(&[0x81, 0x10, 0x01]);
 
     let section = SectionContents {
         section_header: 99,
-        de_data: ArrayView::try_from_slice(&orig_data).unwrap(),
-        data_elements: orig_des,
+        de_region_excl_identity: &de_data,
+        data_element_start_offset: 2,
     };
 
     // extract out the parts of the DE we care about
-    let des = section
-        .data_elements()
-        .map(|de| (de.offset(), de.de_type(), de.contents().to_vec()))
-        .collect::<Vec<_>>();
+    let des = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
     assert_eq!(
         vec![
-            (2.into(), 100_u32.into(), orig_data[0..10].to_vec()),
-            (3.into(), 101_u32.into(), orig_data[10..100].to_vec()),
-            (4.into(), 102_u32.into(), orig_data[100..].to_vec())
+            DataElement {
+                offset: 2.into(),
+                de_type: 5_u32.into(),
+                contents: &[0x01, 0x02, 0x03, 0x04, 0x05]
+            },
+            DataElement { offset: 3.into(), de_type: 16_u32.into(), contents: &[0x01] },
         ],
         des
     );
@@ -467,12 +410,12 @@
     } else {
         panic!("incorrect header");
     };
-    parse_sections(&v1_header, remaining).expect_err("Expected an error");
+    parse_sections(v1_header, remaining).expect_err("Expected an error");
 }
 
 #[test]
 fn do_deserialize_empty_section() {
-    let adv_builder = AdvBuilder::new();
+    let adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
     let adv = adv_builder.into_advertisement();
     let (remaining, header) = parse_adv_header(adv.as_slice()).unwrap();
     let v1_header = if let AdvHeader::V1(h) = header {
@@ -480,12 +423,12 @@
     } else {
         panic!("incorrect header");
     };
-    parse_sections(&v1_header, remaining).expect_err("Expected an error");
+    parse_sections(v1_header, remaining).expect_err("Expected an error");
 }
 
 #[test]
-fn do_deserialize_max_number_of_sections() {
-    let adv_builder = build_dummy_advertisement_sections(NP_V1_ADV_MAX_SECTION_COUNT);
+fn do_deserialize_max_number_of_public_sections() {
+    let adv_builder = build_dummy_advertisement_sections(NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT);
     let adv = adv_builder.into_advertisement();
     let (remaining, header) = parse_adv_header(adv.as_slice()).unwrap();
 
@@ -494,14 +437,13 @@
     } else {
         panic!("incorrect header");
     };
-
-    let sections = parse_sections(&v1_header, remaining).unwrap();
-    assert_eq!(NP_V1_ADV_MAX_SECTION_COUNT, sections.len());
+    let sections = parse_sections(v1_header, remaining).unwrap();
+    assert_eq!(NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT, sections.len());
 }
 
 #[test]
-fn try_deserialize_over_max_number_of_sections() {
-    let adv_builder = build_dummy_advertisement_sections(NP_V1_ADV_MAX_SECTION_COUNT);
+fn try_deserialize_over_max_number_of_public_sections() {
+    let adv_builder = build_dummy_advertisement_sections(NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT);
     let mut adv = adv_builder.into_advertisement().as_slice().to_vec();
 
     // Push an extra section
@@ -520,8 +462,8 @@
     } else {
         panic!("incorrect header");
     };
-    parse_sections(&v1_header, remaining)
-        .expect_err("Expected an error because number of sections is over 16");
+    parse_sections(v1_header, remaining)
+        .expect_err("Expected an error because number of sections is over limit");
 }
 
 pub(crate) fn random_de<R: rand::Rng>(rng: &mut R) -> GenericDataElement {
@@ -533,19 +475,19 @@
     GenericDataElement::try_from(rng.gen_range(20_u32..1000).into(), data.as_slice()).unwrap()
 }
 
-fn do_deserialize_section_unencrypted<I: serialize::SectionIdentity>(
+fn do_deserialize_section_unencrypted<I: serialize::SectionEncoder>(
     identity: I,
     expected_identity: PlaintextIdentityMode,
-    prefix_len: usize,
     de_offset: usize,
 ) {
     let mut rng = rand::rngs::StdRng::from_entropy();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
     let mut section_builder = adv_builder.section_builder(identity).unwrap();
 
-    let (expected_de_data, expected_des, orig_des) =
-        fill_section_random_des(&mut rng, &mut section_builder, de_offset);
+    let mut expected_de_data = vec![];
+    let (expected_des, orig_des) =
+        fill_section_random_des(&mut rng, &mut expected_de_data, &mut section_builder, de_offset);
 
     section_builder.add_to_advertisement();
 
@@ -559,41 +501,33 @@
         panic!("incorrect header");
     };
 
-    let sections = parse_sections(&v1_header, remaining).unwrap();
+    let sections = parse_sections(v1_header, remaining).unwrap();
     assert_eq!(1, sections.len());
     let section = sections[0].as_plaintext().unwrap();
 
+    assert_eq!(section.identity(), expected_identity);
+    let data_elements = section.collect_data_elements().unwrap();
+    assert_eq!(data_elements, expected_des);
     assert_eq!(
-        &PlaintextSection {
-            identity: expected_identity,
-            contents: SectionContents {
-                section_header: (prefix_len + total_de_len(&orig_des)) as u8,
-                de_data: ArrayView::try_from_slice(expected_de_data.as_slice()).unwrap(),
-                data_elements: expected_des,
-            }
-        },
-        section
-    );
-    assert_eq!(
-        section
-            .contents
-            .data_elements()
+        data_elements
+            .iter()
             .map(|de| GenericDataElement::try_from(de.de_type(), de.contents()).unwrap())
             .collect::<Vec<_>>(),
         orig_des
     );
 }
 
-fn fill_section_random_des<R: rand::Rng, I: serialize::SectionIdentity>(
+fn fill_section_random_des<'adv, R: rand::Rng, I: serialize::SectionEncoder>(
     mut rng: &mut R,
+    sink: &'adv mut Vec<u8>,
     section_builder: &mut SectionBuilder<I>,
     de_offset: usize,
-) -> (Vec<u8>, Vec<OffsetDataElement>, Vec<GenericDataElement>) {
-    let mut expected_de_data = vec![];
+) -> (Vec<DataElement<'adv>>, Vec<GenericDataElement>) {
     let mut expected_des = vec![];
     let mut orig_des = vec![];
+    let mut de_ranges = vec![];
 
-    for index in 0..rng.gen_range(1..10) {
+    for _ in 0..rng.gen_range(1..10) {
         let de = random_de(&mut rng);
 
         let de_clone = de.clone();
@@ -601,19 +535,21 @@
             break;
         }
 
-        let orig_len = expected_de_data.len();
-        de.write_de_contents(&mut expected_de_data).unwrap();
-        let contents_len = expected_de_data.len() - orig_len;
-
-        expected_des.push(OffsetDataElement {
-            offset: (index as usize + de_offset).into(),
-            de_type: de.de_header().de_type,
-            contents_len,
-            start_of_contents: orig_len,
-        });
+        let orig_len = sink.len();
+        de.write_de_contents(sink).unwrap();
+        let contents_len = sink.len() - orig_len;
+        de_ranges.push(orig_len..orig_len + contents_len);
         orig_des.push(de);
     }
-    (expected_de_data, expected_des, orig_des)
+
+    for (index, (de, range)) in orig_des.iter().zip(de_ranges).enumerate() {
+        expected_des.push(DataElement {
+            offset: u8::try_from(index + de_offset).unwrap().into(),
+            de_type: de.de_header().de_type,
+            contents: &sink[range],
+        });
+    }
+    (expected_des, orig_des)
 }
 
 fn total_de_len(des: &[GenericDataElement]) -> usize {
@@ -626,29 +562,38 @@
         .sum()
 }
 
-type TryDeserOutput<'c, C> = Option<(DecryptedSection, <C as MatchableCredential>::Matched<'c>)>;
+type TryDeserOutput<'adv, M> = Option<WithMatchedCredential<M, DecryptedSection<'adv>>>;
 
 /// Returns:
 /// - `Ok(Some)` if a matching credential was found
 /// - `Ok(None)` if no matching credential was found, or if `cred_source` provides no credentials
 /// - `Err` if an error occurred.
-fn try_deserialize_all_creds<'c, C, S, P>(
-    section: &CiphertextSection,
-    cred_source: &'c S,
-) -> Result<TryDeserOutput<'c, C>, BatchSectionDeserializeError>
+fn try_deserialize_all_creds<'a, S, P>(
+    mut arena: impl BorrowMut<DeserializationArena<'a>>,
+    section: &'a CiphertextSection,
+    cred_source: &'a S,
+) -> Result<TryDeserOutput<'a, S::Matched>, BatchSectionDeserializeError>
 where
-    C: V1Credential,
-    S: CredentialSource<C>,
+    S: DiscoveryCredentialSource<'a, V1>,
     P: CryptoProvider,
 {
-    for c in cred_source.iter() {
-        match section.try_deserialize::<C, P>(c) {
-            Ok(s) => return Ok(Some((s, c.matched()))),
+    for (crypto_material, match_data) in cred_source.iter() {
+        match section.try_resolve_identity_and_deserialize::<_, P>(
+            arena.borrow_mut(),
+            crypto_material.borrow(),
+        ) {
+            Ok(s) => {
+                let metadata_nonce = crypto_material.metadata_nonce::<P>();
+                return Ok(Some(WithMatchedCredential::new(match_data, metadata_nonce, s)));
+            }
             Err(e) => match e {
                 SectionDeserializeError::IncorrectCredential => continue,
                 SectionDeserializeError::ParseError => {
                     return Err(BatchSectionDeserializeError::ParseError)
                 }
+                SectionDeserializeError::ArenaOutOfSpace => {
+                    return Err(BatchSectionDeserializeError::ArenaOutOfSpace)
+                }
             },
         }
     }
@@ -657,9 +602,9 @@
 }
 
 fn build_dummy_advertisement_sections(number_of_sections: usize) -> AdvBuilder {
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
     for _ in 0..number_of_sections {
-        let section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+        let section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
         section_builder.add_to_advertisement();
     }
     adv_builder
@@ -669,4 +614,6 @@
 enum BatchSectionDeserializeError {
     /// Advertisement data is malformed
     ParseError,
+    /// The given arena is not large enough to hold the decrypted data
+    ArenaOutOfSpace,
 }
diff --git a/nearby/presence/np_adv/src/extended/deserialize/test_stubs.rs b/nearby/presence/np_adv/src/extended/deserialize/test_stubs.rs
index 437b997..68b6f5d 100644
--- a/nearby/presence/np_adv/src/extended/deserialize/test_stubs.rs
+++ b/nearby/presence/np_adv/src/extended/deserialize/test_stubs.rs
@@ -14,99 +14,29 @@
 
 extern crate std;
 
-use crypto_provider::{ed25519, CryptoProvider};
-use np_hkdf::{NpKeySeedHkdf, UnsignedSectionKeys};
 use std::prelude::rust_2021::*;
 
 use crate::{
-    credential::v1::*,
     extended::deserialize::{CiphertextSection, PlaintextSection},
     IntermediateSection,
 };
 
-pub(crate) struct HkdfCryptoMaterial {
-    pub(crate) hkdf: [u8; 32],
-    pub(crate) expected_unsigned_metadata_key_hmac: [u8; 32],
-    pub(crate) expected_signed_metadata_key_hmac: [u8; 32],
-    pub(crate) pub_key: ed25519::RawPublicKey,
-}
-
-impl HkdfCryptoMaterial {
-    pub(crate) fn new<C: CryptoProvider>(
-        hkdf_key_seed: &[u8; 32],
-        metadata_key: &[u8; 16],
-        pub_key: np_ed25519::PublicKey<C>,
-    ) -> Self {
-        let hkdf = NpKeySeedHkdf::<C>::new(hkdf_key_seed);
-        let unsigned =
-            hkdf.extended_unsigned_metadata_key_hmac_key().calculate_hmac(metadata_key.as_slice());
-        let signed =
-            hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(metadata_key.as_slice());
-        Self {
-            hkdf: *hkdf_key_seed,
-            expected_unsigned_metadata_key_hmac: unsigned,
-            expected_signed_metadata_key_hmac: signed,
-            pub_key: pub_key.to_bytes(),
-        }
-    }
-}
-
-impl HkdfCryptoMaterial {
-    fn hkdf<C: CryptoProvider>(&self) -> NpKeySeedHkdf<C> {
-        NpKeySeedHkdf::<C>::new(&self.hkdf)
-    }
-}
-
-impl V1CryptoMaterial for HkdfCryptoMaterial {
-    type SignedIdentityResolverReference<'a> = SignedSectionIdentityResolutionMaterial
-        where Self: 'a;
-    type UnsignedIdentityResolverReference<'a> = UnsignedSectionIdentityResolutionMaterial
-        where Self: 'a;
-
-    fn signed_identity_resolution_material<C: CryptoProvider>(
-        &self,
-    ) -> Self::SignedIdentityResolverReference<'_> {
-        SignedSectionIdentityResolutionMaterial::from_hkdf_and_expected_metadata_key_hmac::<C>(
-            &self.hkdf::<C>(),
-            self.expected_signed_metadata_key_hmac,
-        )
-    }
-    fn unsigned_identity_resolution_material<C: CryptoProvider>(
-        &self,
-    ) -> Self::UnsignedIdentityResolverReference<'_> {
-        UnsignedSectionIdentityResolutionMaterial::from_hkdf_and_expected_metadata_key_hmac::<C>(
-            &self.hkdf::<C>(),
-            self.expected_unsigned_metadata_key_hmac,
-        )
-    }
-    fn signed_verification_material<C: CryptoProvider>(&self) -> SignedSectionVerificationMaterial {
-        SignedSectionVerificationMaterial { pub_key: self.pub_key }
-    }
-
-    fn unsigned_verification_material<C: CryptoProvider>(
-        &self,
-    ) -> UnsignedSectionVerificationMaterial {
-        let mic_hmac_key = *UnsignedSectionKeys::hmac_key(&self.hkdf::<C>()).as_bytes();
-        UnsignedSectionVerificationMaterial { mic_hmac_key }
-    }
-}
-
-pub(crate) trait IntermediateSectionExt {
+pub(crate) trait IntermediateSectionExt<'adv> {
     /// Returns `Some` if `self` is `Plaintext`
-    fn as_plaintext(&self) -> Option<&PlaintextSection>;
+    fn as_plaintext(&self) -> Option<&PlaintextSection<'adv>>;
     /// Returns `Some` if `self` is `Ciphertext`
-    fn as_ciphertext(&self) -> Option<&CiphertextSection>;
+    fn as_ciphertext(&self) -> Option<&CiphertextSection<'adv>>;
 }
 
-impl<'a> IntermediateSectionExt for IntermediateSection<'a> {
-    fn as_plaintext(&self) -> Option<&PlaintextSection> {
+impl<'adv> IntermediateSectionExt<'adv> for IntermediateSection<'adv> {
+    fn as_plaintext(&self) -> Option<&PlaintextSection<'adv>> {
         match self {
             IntermediateSection::Plaintext(s) => Some(s),
             IntermediateSection::Ciphertext(_) => None,
         }
     }
 
-    fn as_ciphertext(&self) -> Option<&CiphertextSection> {
+    fn as_ciphertext(&self) -> Option<&CiphertextSection<'adv>> {
         match self {
             IntermediateSection::Plaintext(_) => None,
             IntermediateSection::Ciphertext(s) => Some(s),
diff --git a/nearby/presence/np_adv/src/extended/mod.rs b/nearby/presence/np_adv/src/extended/mod.rs
index c2f485e..68b16c3 100644
--- a/nearby/presence/np_adv/src/extended/mod.rs
+++ b/nearby/presence/np_adv/src/extended/mod.rs
@@ -31,7 +31,10 @@
     - 2;
 
 /// Maximum number of sections in an advertisement
-pub const NP_V1_ADV_MAX_SECTION_COUNT: usize = 8;
+pub const NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT: usize = 8;
+
+/// Maximum number of public sections in an advertisement
+pub const NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT: usize = 1;
 
 /// Maximum size of a NP section, including its header byte
 pub const NP_ADV_MAX_SECTION_LEN: usize = BLE_ADV_SVC_CONTENT_LEN
@@ -53,8 +56,8 @@
     /// A convenient constant for zero length.
     pub const ZERO: DeLength = DeLength { len: 0 };
 
-    fn as_usize(&self) -> usize {
-        self.len as usize
+    fn as_u8(&self) -> u8 {
+        self.len
     }
 }
 
@@ -62,7 +65,7 @@
     type Error = DeLengthOutOfRange;
 
     fn try_from(value: u8) -> Result<Self, Self::Error> {
-        if value as usize <= MAX_DE_LEN {
+        if usize::from(value) <= MAX_DE_LEN {
             Ok(Self { len: value })
         } else {
             Err(DeLengthOutOfRange {})
diff --git a/nearby/presence/np_adv/src/extended/section_signature_payload.rs b/nearby/presence/np_adv/src/extended/section_signature_payload.rs
index 2b0ebe5..d8ec93c 100644
--- a/nearby/presence/np_adv/src/extended/section_signature_payload.rs
+++ b/nearby/presence/np_adv/src/extended/section_signature_payload.rs
@@ -17,6 +17,7 @@
 //! performing signatures and signature verification.
 
 use crate::extended::deserialize::EncryptionInfo;
+use crate::MetadataKey;
 use crate::NP_SVC_UUID;
 use crypto_provider::{aes::ctr::AesCtrNonce, CryptoProvider};
 use sink::{Sink, SinkWriter};
@@ -45,10 +46,10 @@
     /// to be included in the signature payload after the derived salt,
     /// and before the signature itself.
     Raw(&'a [u8]),
-    /// References to the plaintext identity DE header and the rest
-    /// of the section plaintext after that (includes the plaintext
-    /// identity DE payload).
-    IdentityHeaderThenRaw([u8; 2], &'a [u8]),
+    /// Plaintext identity DE header followed by the metadata key,
+    /// then the rest of the section plaintext (including
+    /// the plaintext identity DE payload).
+    IdentityHeaderMetadataKeyAndRemainder([u8; 2], MetadataKey, &'a [u8]),
 }
 
 const ADV_SIGNATURE_CONTEXT: np_ed25519::SignatureContext = {
@@ -60,6 +61,30 @@
 
 impl<'a> SectionSignaturePayload<'a> {
     /// Construct a section signature payload using parts typically found during
+    /// deserialization of advertisements.
+    pub(crate) fn from_deserialized_parts(
+        adv_header_byte: u8,
+        section_header: u8,
+        encryption_info: &'a [u8; EncryptionInfo::TOTAL_DE_LEN],
+        nonce_ref: &'a AesCtrNonce,
+        identity_header: [u8; 2],
+        plaintext_metadata_key: MetadataKey,
+        raw_plaintext_remainder: &'a [u8],
+    ) -> Self {
+        Self {
+            adv_header_byte,
+            section_header,
+            encryption_info,
+            nonce_ref,
+            after_iv_info: AfterIVInfo::IdentityHeaderMetadataKeyAndRemainder(
+                identity_header,
+                plaintext_metadata_key,
+                raw_plaintext_remainder,
+            ),
+        }
+    }
+
+    /// Construct a section signature payload using parts typically found during
     /// serialization of advertisements.
     pub(crate) fn from_serialized_parts(
         adv_header_byte: u8,
@@ -77,28 +102,6 @@
         }
     }
 
-    /// Construct a section signature payload using parts typically found during
-    /// deserialization of advertisements.
-    pub(crate) fn from_deserialized_parts(
-        adv_header_byte: u8,
-        section_header: u8,
-        encryption_info: &'a [u8; EncryptionInfo::TOTAL_DE_LEN],
-        nonce_ref: &'a AesCtrNonce,
-        identity_header: [u8; 2],
-        raw_after_identity_header_info: &'a [u8],
-    ) -> Self {
-        Self {
-            adv_header_byte,
-            section_header,
-            encryption_info,
-            nonce_ref,
-            after_iv_info: AfterIVInfo::IdentityHeaderThenRaw(
-                identity_header,
-                raw_after_identity_header_info,
-            ),
-        }
-    }
-
     /// Generates a signature for this section signing payload using
     /// the given Ed25519 key-pair.
     pub(crate) fn sign<C: CryptoProvider>(
@@ -133,9 +136,14 @@
         // identity DE and the rest of the DEs except for the suffix
         match self.after_iv_info {
             AfterIVInfo::Raw(s) => sink.try_extend_from_slice(s),
-            AfterIVInfo::IdentityHeaderThenRaw(identity_header, s) => {
+            AfterIVInfo::IdentityHeaderMetadataKeyAndRemainder(
+                identity_header,
+                metadata_key,
+                remainder,
+            ) => {
                 sink.try_extend_from_slice(&identity_header)?;
-                sink.try_extend_from_slice(s)
+                sink.try_extend_from_slice(&metadata_key.0)?;
+                sink.try_extend_from_slice(remainder)
             }
         }
     }
diff --git a/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs b/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs
index 47804ea..36e4e5f 100644
--- a/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/adv_tests.rs
@@ -20,18 +20,10 @@
 
 #[test]
 fn adv_encode_no_salt() {
-    let mut adv_builder = AdvBuilder::new();
-
-    let mut no_identity_section_builder =
-        adv_builder.section_builder(PublicIdentity::default()).unwrap();
-    no_identity_section_builder
-        .add_de(|_| DummyDataElement { de_type: 20_u32.into(), data: vec![] })
-        .unwrap();
-
-    no_identity_section_builder.add_to_advertisement();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
 
     let mut public_identity_section_builder =
-        adv_builder.section_builder(PublicIdentity::default()).unwrap();
+        adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
     public_identity_section_builder
         .add_de(|_| DummyDataElement { de_type: 30_u32.into(), data: vec![] })
         .unwrap();
@@ -43,9 +35,6 @@
             0x20, // adv header
             0x3,  // section header
             0x3,  // public identity
-            0x80, 20,  // de header
-            0x3, // section header
-            0x3, // public identity
             0x80, 30, // de header
         ],
         adv_builder.into_advertisement().as_slice()
@@ -53,45 +42,12 @@
 }
 
 #[test]
-fn adv_encode_with_salt() {
-    let mut adv_builder = AdvBuilder::new();
-
-    let mut no_identity_section_builder =
-        adv_builder.section_builder(PublicIdentity::default()).unwrap();
-    no_identity_section_builder
-        .add_de(|_| DummyDataElement { de_type: 20_u32.into(), data: vec![] })
-        .unwrap();
-
-    no_identity_section_builder.add_to_advertisement();
-
-    let mut public_identity_section_builder =
-        adv_builder.section_builder(PublicIdentity::default()).unwrap();
-    public_identity_section_builder
-        .add_de(|_| DummyDataElement { de_type: 30_u32.into(), data: vec![] })
-        .unwrap();
-
-    public_identity_section_builder.add_to_advertisement();
-
-    let mut expected = vec![
-        0x20, // adv header
-    ];
-    expected.extend_from_slice(&[
-        0x3, // section header
-        0x3, // public identity
-        0x80, 20,  // de header
-        0x3, // section header
-        0x3, // public identity
-        0x80, 30, // de header
-    ]);
-    assert_eq!(expected, adv_builder.into_advertisement().as_slice())
-}
-
-#[test]
 fn adding_any_allowed_section_length_always_works_for_single_section() {
     // up to section len - 1 to leave room for section header
     for section_contents_len in 0..NP_ADV_MAX_SECTION_LEN - 1 {
-        let mut adv_builder = AdvBuilder::new();
-        let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+        let mut section_builder =
+            adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
         fill_section_builder(section_contents_len, &mut section_builder);
 
         section_builder.add_to_advertisement();
@@ -108,50 +64,17 @@
 }
 
 #[test]
-fn adding_any_allowed_section_length_always_works_for_two_sections() {
-    // leave room for both section header bytes and the public identities
-    for section_1_contents_len in 0..=NP_ADV_MAX_SECTION_LEN - 4 {
-        // leave room for both section headers + public identities + section 1 contents
-        for section_2_contents_len in 0..=NP_ADV_MAX_SECTION_LEN - section_1_contents_len - 4 {
-            let mut adv_builder = AdvBuilder::new();
-
-            let mut section_1_builder =
-                adv_builder.section_builder(PublicIdentity::default()).unwrap();
-            fill_section_builder(section_1_contents_len, &mut section_1_builder);
-            section_1_builder.add_to_advertisement();
-
-            let mut section_2_builder =
-                adv_builder.section_builder(PublicIdentity::default()).unwrap();
-            fill_section_builder(section_2_contents_len, &mut section_2_builder);
-            section_2_builder.add_to_advertisement();
-
-            let adv = adv_builder.into_advertisement();
-            assert_eq!(
-                section_1_contents_len + section_2_contents_len + 1 + 4, // adv and section headers
-                adv.as_slice().len(),
-                "adv: {:?}\nsection 1 contents len: {}, section 2 contents len: {}",
-                adv.as_slice(),
-                section_1_contents_len,
-                section_2_contents_len,
-            );
-        }
-    }
-}
-
-#[test]
 fn building_capacity_0_section_works() {
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
 
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
-    // leave room for two section headers and the public identities
-    fill_section_builder(NP_ADV_MAX_SECTION_LEN - 4, &mut section_builder);
-    section_builder.add_to_advertisement();
+    // leave room for section header and the public identity
+    fill_section_builder(NP_ADV_MAX_SECTION_LEN - 2, &mut section_builder);
 
-    let section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
-    // The section header and the public identity
-    assert_eq!(2, section_builder.section.capacity);
-    assert_eq!(2, section_builder.section.len());
+    assert_eq!(NP_ADV_MAX_SECTION_LEN, section_builder.section.capacity);
+    assert_eq!(NP_ADV_MAX_SECTION_LEN, section_builder.section.len());
+
     section_builder.add_to_advertisement();
 
     assert_eq!(BLE_ADV_SVC_CONTENT_LEN, adv_builder.into_advertisement().as_slice().len());
@@ -161,10 +84,11 @@
 #[derive(Default, PartialEq, Eq, Debug)]
 struct EnormousIdentity {}
 
-impl SectionIdentity for EnormousIdentity {
+impl SectionEncoder for EnormousIdentity {
     const PREFIX_LEN: usize = 200;
     const SUFFIX_LEN: usize = 0;
     const INITIAL_DE_OFFSET: DataElementOffset = DataElementOffset::ZERO;
+    const ADVERTISEMENT_TYPE: AdvertisementType = AdvertisementType::Plaintext;
 
     fn postprocess(
         &mut self,
@@ -176,7 +100,6 @@
     }
 
     type DerivedSalt = ();
-
     fn de_salt(&self, _de_offset: DataElementOffset) -> Self::DerivedSalt {
         panic!("should never be called, just used for its huge prefix")
     }
diff --git a/nearby/presence/np_adv/src/extended/serialize/mod.rs b/nearby/presence/np_adv/src/extended/serialize/mod.rs
index 6cedd1e..f736dea 100644
--- a/nearby/presence/np_adv/src/extended/serialize/mod.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/mod.rs
@@ -26,8 +26,8 @@
 //! use np_adv::shared_data::TxPower;
 //!
 //! // no section identities or DEs need salt in this example
-//! let mut adv_builder = AdvBuilder::new();
-//! let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+//! let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+//! let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 //!
 //! section_builder.add_de(|_salt| TxPowerDataElement::from(TxPower::try_from(3).unwrap())).unwrap();
 //!
@@ -54,28 +54,35 @@
 //!
 //! ```
 //! use np_adv::{
+//!     credential::{SimpleBroadcastCryptoMaterial, v1::V1},
 //!     de_type::EncryptedIdentityDataElementType,
 //!     extended::{data_elements::*, serialize::*, de_type::DeType },
+//!     MetadataKey,
 //! };
 //! use rand::{Rng as _, SeedableRng as _};
 //! use crypto_provider::{CryptoProvider, CryptoRng};
 //! use crypto_provider_default::CryptoProviderImpl;
 //! use np_adv::shared_data::TxPower;
 //!
-//! let mut adv_builder = AdvBuilder::new();
+//! let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 //!
 //! // these would come from the credential//!
 //! let mut rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
 //! let metadata_key: [u8; 16] = rng.gen();
+//! let metadata_key = MetadataKey(metadata_key);
 //! let key_seed: [u8; 32] = rng.gen();
 //! // use your preferred crypto impl
 //! let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 //!
-//! let mut section_builder = adv_builder.section_builder(MicEncrypted::new_random_salt(
+//! let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(
+//!     key_seed,
+//!     metadata_key,
+//! );
+//!
+//! let mut section_builder = adv_builder.section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
 //!     &mut rng,
 //!     EncryptedIdentityDataElementType::Private,
-//!     &metadata_key,
-//!     &key_seed_hkdf,
+//!     &broadcast_cm,
 //! )).unwrap();
 //!
 //! section_builder.add_de(|_salt| TxPowerDataElement::from(TxPower::try_from(3).unwrap())).unwrap();
@@ -104,8 +111,12 @@
 //!         .try_into().expect("array sizes match")
 //! }
 //! ```
-use crate::extended::NP_V1_ADV_MAX_SECTION_COUNT;
+use crate::extended::{NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT, NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT};
 use crate::{
+    credential::{
+        v1::{SignedBroadcastCryptoMaterial, V1},
+        BroadcastCryptoMaterial,
+    },
     de_type::{EncryptedIdentityDataElementType, IdentityDataElementType},
     extended::{
         data_elements::EncryptionInfoDataElement,
@@ -114,11 +125,10 @@
         section_signature_payload::*,
         to_array_view, DeLength, BLE_ADV_SVC_CONTENT_LEN, NP_ADV_MAX_SECTION_LEN,
     },
-    DeLengthOutOfRange, PublicIdentity, NP_SVC_UUID,
+    DeLengthOutOfRange, MetadataKey, NP_SVC_UUID,
 };
 use array_view::ArrayView;
-use core::fmt;
-use core::marker::PhantomData;
+use core::fmt::{self, Display};
 use crypto_provider::{
     aes::{
         ctr::{AesCtr, AesCtrNonce, NonceAndCounter},
@@ -145,36 +155,35 @@
 pub struct AdvBuilder {
     /// Contains the adv header byte
     adv: tinyvec::ArrayVec<[u8; BLE_ADV_SVC_CONTENT_LEN]>,
+    /// To track the number of sections that are in the advertisement
     section_count: usize,
+    /// Advertisement type: Public or Encrypted
+    advertisement_type: AdvertisementType,
 }
 
 impl AdvBuilder {
     /// Build an [AdvBuilder].
-    pub fn new() -> AdvBuilder {
+    pub fn new(advertisement_type: AdvertisementType) -> Self {
         let mut adv = tinyvec::ArrayVec::new();
         // version 1, 0bVVVRRRRR
         adv.push(0b00100000);
-
-        AdvBuilder { adv, section_count: 0 }
+        Self { adv, section_count: 0, advertisement_type }
     }
 
     /// Create a section builder.
     ///
-    /// Returns `Err` if there isn't room in the advertisement for the section at its minimum length
-    /// with its chosen identity.
-    ///
     /// The builder will not accept more DEs than can fit given the space already used in the
     /// advertisement by previous sections, if any.
     ///
     /// Once the builder is populated, add it to the originating advertisement with
     /// [SectionBuilder.add_to_advertisement].
-    pub fn section_builder<I: SectionIdentity>(
+    pub fn section_builder<SE: SectionEncoder>(
         &mut self,
-        identity: I,
-    ) -> Result<SectionBuilder<I>, AddSectionError> {
+        section_encoder: SE,
+    ) -> Result<SectionBuilder<SE>, AddSectionError> {
         // section header and identity prefix
-        let prefix_len = 1 + I::PREFIX_LEN;
-        let minimum_section_len = prefix_len + I::SUFFIX_LEN;
+        let prefix_len = 1 + SE::PREFIX_LEN;
+        let minimum_section_len = prefix_len + SE::SUFFIX_LEN;
         // the max overall len available to the section
         let available_len = self.adv.capacity() - self.adv.len();
 
@@ -182,11 +191,15 @@
             return Err(AddSectionError::InsufficientAdvSpace);
         }
 
-        if self.section_count >= NP_V1_ADV_MAX_SECTION_COUNT {
+        if self.section_count >= self.advertisement_type.max_sections() {
             return Err(AddSectionError::MaxSectionCountExceeded);
         }
 
-        let mut section = tinyvec::ArrayVec::new();
+        if self.advertisement_type != SE::ADVERTISEMENT_TYPE {
+            return Err(AddSectionError::IncompatibleSectionType);
+        }
+
+        let mut section: tinyvec::ArrayVec<[u8; 249]> = tinyvec::ArrayVec::new();
         // placeholder for section header and identity prefix
         section.resize(prefix_len, 0);
 
@@ -194,11 +207,11 @@
             section: CapacityLimitedVec {
                 vec: section,
                 // won't underflow: checked above
-                capacity: available_len - I::SUFFIX_LEN,
+                capacity: available_len - SE::SUFFIX_LEN,
             },
-            identity,
+            section_encoder,
             adv_builder: self,
-            next_de_offset: I::INITIAL_DE_OFFSET,
+            next_de_offset: SE::INITIAL_DE_OFFSET,
         })
     }
 
@@ -221,33 +234,30 @@
     }
 }
 
-impl Default for AdvBuilder {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 /// Errors that can occur when adding a section to an advertisement
 #[derive(Debug, PartialEq, Eq)]
 pub enum AddSectionError {
     /// The advertisement doesn't have enough space to hold the minimum size of the section
     InsufficientAdvSpace,
-    /// The advertisement can only hold a maximum of NP_V1_ADV_MAX_SECTION_COUNT number of sections
+    /// The advertisement can only hold a maximum of NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT number of sections
     MaxSectionCountExceeded,
+    /// An incompatible section trying to be added
+    IncompatibleSectionType,
 }
 
-/// Derived salt for an individual data element.
-pub struct DeSalt<C: CryptoProvider> {
-    salt: V1Salt<C>,
-    de_offset: DataElementOffset,
-}
-
-impl<C: CryptoProvider> DeSalt<C> {
-    /// Derive salt of the requested length.
-    ///
-    /// The length must be a valid HKDF-SHA256 length.
-    pub fn derive<const N: usize>(&self) -> Option<[u8; N]> {
-        self.salt.derive(Some(self.de_offset))
+impl Display for AddSectionError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            AddSectionError::InsufficientAdvSpace => {
+                write!(f, "The advertisement (max {BLE_ADV_SVC_CONTENT_LEN} bytes) doesn't have enough remaining space to hold the section")
+            }
+            AddSectionError::MaxSectionCountExceeded => {
+                write!(f, "The advertisement can only hold a maximum of {NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT} number of sections")
+            }
+            AddSectionError::IncompatibleSectionType => {
+                write!(f, "Public and Encrypted sections cannot be mixed in the same advertisement")
+            }
+        }
     }
 }
 
@@ -269,22 +279,22 @@
 
 /// Accumulates data elements and encodes them into a section.
 #[derive(Debug)]
-pub struct SectionBuilder<'a, I: SectionIdentity> {
+pub struct SectionBuilder<'a, SE: SectionEncoder> {
     /// Contains the section header, the identity-specified overhead, and any DEs added
     pub(crate) section: CapacityLimitedVec<u8, NP_ADV_MAX_SECTION_LEN>,
-    identity: I,
+    section_encoder: SE,
     /// mut ref to enforce only one active section builder at a time
     adv_builder: &'a mut AdvBuilder,
     next_de_offset: DataElementOffset,
 }
 
-impl<'a, I: SectionIdentity> SectionBuilder<'a, I> {
+impl<'a, SE: SectionEncoder> SectionBuilder<'a, SE> {
     /// Add this builder to the advertisement that created it.
     pub fn add_to_advertisement(self) {
         let adv_builder = self.adv_builder;
         adv_builder.add_section(Self::build_section(
             self.section.into_inner(),
-            self.identity,
+            self.section_encoder,
             adv_builder,
         ))
     }
@@ -292,11 +302,11 @@
     /// Add a data element to the section with a closure that returns a `Result`.
     ///
     /// The provided `build_de` closure will be invoked with the derived salt for this DE.
-    pub fn add_de_res<W: WriteDataElement, E, F: FnOnce(I::DerivedSalt) -> Result<W, E>>(
+    pub fn add_de_res<W: WriteDataElement, E, F: FnOnce(SE::DerivedSalt) -> Result<W, E>>(
         &mut self,
         build_de: F,
     ) -> Result<(), AddDataElementError<E>> {
-        let writer = build_de(self.identity.de_salt(self.next_de_offset))
+        let writer = build_de(self.section_encoder.de_salt(self.next_de_offset))
             .map_err(AddDataElementError::BuildDeError)?;
 
         let orig_len = self.section.len();
@@ -320,10 +330,10 @@
                 e
             })?;
 
-        if content_len != de_header.len.as_usize() {
+        if content_len != usize::from(de_header.len.as_u8()) {
             panic!(
                 "Buggy WriteDataElement impl: header len {}, actual written len {}",
-                de_header.len.as_usize(),
+                de_header.len.as_u8(),
                 content_len
             );
         }
@@ -336,7 +346,7 @@
     /// Add a data element to the section with a closure that returns the data element directly.
     ///
     /// The provided `build_de` closure will be invoked with the derived salt for this DE.
-    pub fn add_de<W: WriteDataElement, F: FnOnce(I::DerivedSalt) -> W>(
+    pub fn add_de<W: WriteDataElement, F: FnOnce(SE::DerivedSalt) -> W>(
         &mut self,
         build_de: F,
     ) -> Result<(), AddDataElementError<()>> {
@@ -348,11 +358,11 @@
     /// Implemented without self to avoid partial-move issues.
     fn build_section(
         mut section_contents: tinyvec::ArrayVec<[u8; NP_ADV_MAX_SECTION_LEN]>,
-        mut identity: I,
+        mut section_encoder: SE,
         adv_builder: &AdvBuilder,
     ) -> EncodedSection {
         // there is space because the capacity for DEs was restricted to allow it
-        section_contents.resize(section_contents.len() + I::SUFFIX_LEN, 0);
+        section_contents.resize(section_contents.len() + SE::SUFFIX_LEN, 0);
 
         section_contents[0] = section_contents
             .len()
@@ -361,7 +371,7 @@
             .and_then(|len: u8| len.checked_sub(1))
             .expect("section length is always <=255 and non-negative");
 
-        identity.postprocess(
+        section_encoder.postprocess(
             adv_builder.header_byte(),
             section_contents[0],
             &mut section_contents[1..],
@@ -380,8 +390,26 @@
     InsufficientSectionSpace,
 }
 
-/// The identity used for an individual section.
-pub trait SectionIdentity {
+/// The advertisement type, which dictates what sections can exist
+#[derive(Debug, PartialEq, Eq)]
+pub enum AdvertisementType {
+    /// Plaintext advertisement with only plaintext sections
+    Plaintext,
+    /// Encrypted advertisement with only encrypted sections
+    Encrypted,
+}
+
+impl AdvertisementType {
+    fn max_sections(&self) -> usize {
+        match self {
+            AdvertisementType::Plaintext => NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT,
+            AdvertisementType::Encrypted => NP_V1_ADV_MAX_ENCRYPTED_SECTION_COUNT,
+        }
+    }
+}
+
+/// Everything needed to properly encode a section
+pub trait SectionEncoder {
     /// How much space needs to be reserved for this identity's prefix bytes after the section
     /// header and before other DEs
     const PREFIX_LEN: usize;
@@ -392,6 +420,9 @@
     /// The DE offset to use for any DEs added to the section
     const INITIAL_DE_OFFSET: DataElementOffset;
 
+    /// The advertisement type that can support this section
+    const ADVERTISEMENT_TYPE: AdvertisementType;
+
     /// Postprocess the contents of the section (the data after the section header byte), which will
     /// start with [Self::PREFIX_LEN] bytes set aside for the identity's use, and similarly end with
     /// [Self::SUFFIX_LEN] bytes, with DEs (if any) in the middle.
@@ -404,13 +435,16 @@
     fn de_salt(&self, de_offset: DataElementOffset) -> Self::DerivedSalt;
 }
 
-impl SectionIdentity for PublicIdentity {
+/// Public section for plaintext data elements
+#[derive(Default, Debug)]
+pub struct PublicSectionEncoder {}
+impl SectionEncoder for PublicSectionEncoder {
     /// 1 byte of public identity DE header
     const PREFIX_LEN: usize = 1;
     const SUFFIX_LEN: usize = 0;
     /// Room for the public DE
     const INITIAL_DE_OFFSET: DataElementOffset = DataElementOffset::ZERO.incremented();
-
+    const ADVERTISEMENT_TYPE: AdvertisementType = AdvertisementType::Plaintext;
     fn postprocess(
         &mut self,
         _adv_header_byte: u8,
@@ -423,180 +457,50 @@
                 .as_slice(),
         )
     }
-
     type DerivedSalt = ();
-
     fn de_salt(&self, _de_offset: DataElementOffset) -> Self::DerivedSalt {}
 }
 
-/// Encrypts the data elements and protects integrity with a MIC using key material derived from
-/// an NP identity.
-pub struct MicEncrypted<'a, C: CryptoProvider> {
-    identity_type: EncryptedIdentityDataElementType,
-    salt: V1Salt<C>,
-    identity_payload: &'a [u8; 16],
-    aes_key: Aes128Key,
-    mic_hmac_key: np_hkdf::NpHmacSha256Key<C>,
-}
-
-impl<'a, C: CryptoProvider> MicEncrypted<'a, C> {
-    /// Build a [MicEncrypted] from the provided identity info with a random salt.
-    pub fn new_random_salt(
-        rng: &mut C::CryptoRng,
-        identity_type: EncryptedIdentityDataElementType,
-        identity_payload: &'a [u8; 16],
-        keys: &impl np_hkdf::UnsignedSectionKeys<C>,
-    ) -> Self {
-        let salt: V1Salt<C> = rng.gen::<[u8; 16]>().into();
-        Self::new(identity_type, salt, identity_payload, keys)
-    }
-
-    /// Build a [MicEncrypted] from the provided identity info.
-    fn new(
-        identity_type: EncryptedIdentityDataElementType,
-        salt: V1Salt<C>,
-        identity_payload: &'a [u8; 16],
-        keys: &impl np_hkdf::UnsignedSectionKeys<C>,
-    ) -> Self {
-        MicEncrypted {
-            identity_type,
-            salt,
-            identity_payload,
-            aes_key: keys.aes_key(),
-            mic_hmac_key: keys.hmac_key(),
-        }
-    }
-
-    /// Build a [MicEncrypted] from the provided identity info. Exposed outside of this crate for
-    /// testing purposes only.
-    #[cfg(any(test, feature = "testing"))]
-    pub fn new_for_testing(
-        identity_type: EncryptedIdentityDataElementType,
-        salt: V1Salt<C>,
-        identity_payload: &'a [u8; 16],
-        keys: &impl np_hkdf::UnsignedSectionKeys<C>,
-    ) -> Self {
-        Self::new(identity_type, salt, identity_payload, keys)
-    }
-}
-
-impl<'a, C: CryptoProvider> SectionIdentity for MicEncrypted<'a, C> {
-    const PREFIX_LEN: usize =
-        EncryptionInfo::TOTAL_DE_LEN + EncryptedIdentityMetadata::TOTAL_DE_LEN;
-    /// Length of mic
-    const SUFFIX_LEN: usize = SectionMic::CONTENTS_LEN;
-    /// Room for the mic, encryption info, and identity DEs
-    const INITIAL_DE_OFFSET: DataElementOffset =
-        DataElementOffset::ZERO.incremented().incremented();
-
-    fn postprocess(
-        &mut self,
-        adv_header_byte: u8,
-        section_header: u8,
-        section_contents: &mut [u8],
-    ) {
-        // prefix byte layout:
-        // 0-18: Encryption Info DE (header + scheme + salt)
-        // 19-20: Identity DE header
-        // 21-36: Identity DE contents (metadata key)
-
-        // Encryption Info DE
-        let encryption_info_bytes = EncryptionInfoDataElement::mic(
-            self.salt.as_slice().try_into().expect("Salt should be 16 bytes"),
-        )
-        .serialize();
-        section_contents[0..19].copy_from_slice(&encryption_info_bytes);
-
-        // Identity DE
-        let identity_header = identity_de_header(self.identity_type, self.identity_payload);
-        section_contents[19..21].copy_from_slice(identity_header.serialize().as_slice());
-        section_contents[21..37].copy_from_slice(self.identity_payload);
-
-        // DE offset for identity is 1: Encryption Info DE, Identity DE, then other DEs
-        let nonce: AesCtrNonce = self
-            .de_salt(v1_salt::DataElementOffset::from(1))
-            .derive()
-            .expect("AES-CTR nonce is a valid HKDF length");
-
-        let mut cipher = C::AesCtr128::new(&self.aes_key, NonceAndCounter::from_nonce(nonce));
-
-        let ciphertext_end = section_contents.len() - SectionMic::CONTENTS_LEN;
-
-        // encrypt just the part that should be ciphertext: identity DE contents and subsequent DEs
-        cipher.encrypt(&mut section_contents[21..ciphertext_end]);
-
-        // calculate MAC per the spec
-        let mut section_hmac = self.mic_hmac_key.build_hmac();
-        // svc uuid
-        section_hmac.update(NP_SVC_UUID.as_slice());
-        // adv header
-        section_hmac.update(&[adv_header_byte]);
-        // section header
-        section_hmac.update(&[section_header]);
-        // encryption info
-        section_hmac.update(&encryption_info_bytes);
-        // derived salt
-        section_hmac.update(&nonce);
-        // identity header + ciphertext
-        section_hmac.update(&section_contents[19..ciphertext_end]);
-
-        let mic: [u8; 32] = section_hmac.finalize();
-
-        // write truncated MIC
-        section_contents[ciphertext_end..].copy_from_slice(&mic[..SectionMic::CONTENTS_LEN]);
-    }
-
-    type DerivedSalt = DeSalt<C>;
-
-    fn de_salt(&self, de_offset: DataElementOffset) -> Self::DerivedSalt {
-        DeSalt { salt: V1Salt::from(*self.salt.as_array_ref()), de_offset }
-    }
-}
-
 /// Encrypts the data elements and protects integrity with an np_ed25519 signature
 /// using key material derived from an NP identity.
-pub struct SignedEncrypted<'a, C: CryptoProvider> {
+pub struct SignedEncryptedSectionEncoder<C: CryptoProvider> {
     identity_type: EncryptedIdentityDataElementType,
     salt: V1Salt<C>,
-    metadata_key: &'a [u8; 16],
-    key_pair: &'a np_ed25519::KeyPair<C>,
+    metadata_key: MetadataKey,
+    key_pair: np_ed25519::KeyPair<C>,
     aes_key: Aes128Key,
-    _marker: PhantomData<C>,
 }
 
-impl<'a, C: CryptoProvider> SignedEncrypted<'a, C> {
-    /// Build a [SignedEncrypted] from the provided identity material with a random salt.
-    pub fn new_random_salt(
+impl<C: CryptoProvider> SignedEncryptedSectionEncoder<C> {
+    /// Build a [SignedEncryptedSectionEncoder] from an identity type,
+    /// some broadcast crypto-material, and with a random salt.
+    pub fn new_random_salt<B: SignedBroadcastCryptoMaterial>(
         rng: &mut C::CryptoRng,
         identity_type: EncryptedIdentityDataElementType,
-        metadata_key: &'a [u8; 16],
-        key_pair: &'a np_ed25519::KeyPair<C>,
-        key_seed_hkdf: &np_hkdf::NpKeySeedHkdf<C>,
+        crypto_material: &B,
     ) -> Self {
         let salt: V1Salt<C> = rng.gen::<[u8; 16]>().into();
-        Self::new(identity_type, salt, metadata_key, key_pair, key_seed_hkdf)
+        Self::new(identity_type, salt, crypto_material)
     }
 
-    /// Build a [SignedEncrypted] from the provided identity material.
-    pub(crate) fn new(
+    /// Build a [SignedEncryptedSectionEncoder] from an identity type,
+    /// a provided salt, and some broadcast crypto-material.
+    pub(crate) fn new<B: SignedBroadcastCryptoMaterial>(
         identity_type: EncryptedIdentityDataElementType,
         salt: V1Salt<C>,
-        metadata_key: &'a [u8; 16],
-        key_pair: &'a np_ed25519::KeyPair<C>,
-        key_seed_hkdf: &np_hkdf::NpKeySeedHkdf<C>,
+        crypto_material: &B,
     ) -> Self {
-        Self {
-            identity_type,
-            salt,
-            metadata_key,
-            key_pair,
-            aes_key: key_seed_hkdf.extended_signed_section_aes_key(),
-            _marker: Default::default(),
-        }
+        let metadata_key = crypto_material.metadata_key();
+        let key_seed = crypto_material.key_seed();
+        let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&key_seed);
+        let private_key = crypto_material.signing_key();
+        let key_pair = np_ed25519::KeyPair::<C>::from_private_key(&private_key);
+        let aes_key = key_seed_hkdf.extended_signed_section_aes_key();
+        Self { identity_type, salt, metadata_key, key_pair, aes_key }
     }
 }
 
-impl<'a, C: CryptoProvider> SectionIdentity for SignedEncrypted<'a, C> {
+impl<C: CryptoProvider> SectionEncoder for SignedEncryptedSectionEncoder<C> {
     const PREFIX_LEN: usize =
         EncryptionInfo::TOTAL_DE_LEN + EncryptedIdentityMetadata::TOTAL_DE_LEN;
     /// Ed25519 signature
@@ -604,6 +508,7 @@
     /// Room for the encryption info and identity DEs
     const INITIAL_DE_OFFSET: DataElementOffset =
         DataElementOffset::ZERO.incremented().incremented();
+    const ADVERTISEMENT_TYPE: AdvertisementType = AdvertisementType::Encrypted;
 
     fn postprocess(
         &mut self,
@@ -619,7 +524,7 @@
 
         let identity_header = identity_de_header(self.identity_type, self.metadata_key);
         section_contents[19..21].copy_from_slice(identity_header.serialize().as_slice());
-        section_contents[21..37].copy_from_slice(self.metadata_key);
+        section_contents[21..37].copy_from_slice(&self.metadata_key.0);
 
         let nonce: AesCtrNonce = self
             .de_salt(v1_salt::DataElementOffset::from(1))
@@ -648,14 +553,14 @@
             after_encryption_info,
         );
 
-        let signature = section_signature_payload.sign(self.key_pair);
+        let signature = section_signature_payload.sign(&self.key_pair);
 
         sig[0..64].copy_from_slice(&signature.to_bytes());
 
         let mut cipher = C::AesCtr128::new(&self.aes_key, NonceAndCounter::from_nonce(nonce));
 
         // encrypt just the part that should be ciphertext: identity DE contents and subsequent DEs
-        cipher.encrypt(&mut section_contents[21..]);
+        cipher.apply_keystream(&mut section_contents[21..]);
     }
 
     type DerivedSalt = DeSalt<C>;
@@ -665,6 +570,135 @@
     }
 }
 
+/// Encrypts the data elements and protects integrity with a MIC using key material derived from
+/// an NP identity.
+pub struct MicEncryptedSectionEncoder<C: CryptoProvider> {
+    identity_type: EncryptedIdentityDataElementType,
+    salt: V1Salt<C>,
+    metadata_key: MetadataKey,
+    aes_key: Aes128Key,
+    mic_hmac_key: np_hkdf::NpHmacSha256Key<C>,
+}
+
+impl<C: CryptoProvider> MicEncryptedSectionEncoder<C> {
+    /// Build a [MicEncryptedSectionEncoder] from the provided identity
+    /// info with a random salt.
+    pub fn new_random_salt<B: BroadcastCryptoMaterial<V1>>(
+        rng: &mut C::CryptoRng,
+        identity_type: EncryptedIdentityDataElementType,
+        crypto_material: &B,
+    ) -> Self {
+        let salt: V1Salt<C> = rng.gen::<[u8; 16]>().into();
+        Self::new(identity_type, salt, crypto_material)
+    }
+
+    /// Build a [MicEncryptedSectionEncoder] from the provided identity info.
+    pub(crate) fn new<B: BroadcastCryptoMaterial<V1>>(
+        identity_type: EncryptedIdentityDataElementType,
+        salt: V1Salt<C>,
+        crypto_material: &B,
+    ) -> Self {
+        let metadata_key = crypto_material.metadata_key();
+        let key_seed = crypto_material.key_seed();
+        let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&key_seed);
+        let aes_key = np_hkdf::UnsignedSectionKeys::aes_key(&key_seed_hkdf);
+        let mic_hmac_key = np_hkdf::UnsignedSectionKeys::hmac_key(&key_seed_hkdf);
+
+        Self { identity_type, salt, metadata_key, aes_key, mic_hmac_key }
+    }
+
+    /// Build a [MicEncrypedSectionEncoder] from the provided identity info.
+    /// Exposed outside of this crate for testing purposes only, since this
+    /// does not handle the generation of random salts.
+    #[cfg(any(test, feature = "testing"))]
+    pub fn new_for_testing<B: BroadcastCryptoMaterial<V1>>(
+        identity_type: EncryptedIdentityDataElementType,
+        salt: V1Salt<C>,
+        crypto_material: &B,
+    ) -> Self {
+        Self::new(identity_type, salt, crypto_material)
+    }
+}
+
+impl<C: CryptoProvider> SectionEncoder for MicEncryptedSectionEncoder<C> {
+    const PREFIX_LEN: usize =
+        EncryptionInfo::TOTAL_DE_LEN + EncryptedIdentityMetadata::TOTAL_DE_LEN;
+    /// Length of mic
+    const SUFFIX_LEN: usize = SectionMic::CONTENTS_LEN;
+    /// Room for the mic, encryption info, and identity DEs
+    const INITIAL_DE_OFFSET: DataElementOffset =
+        DataElementOffset::ZERO.incremented().incremented();
+
+    const ADVERTISEMENT_TYPE: AdvertisementType = AdvertisementType::Encrypted;
+
+    fn postprocess(
+        &mut self,
+        adv_header_byte: u8,
+        section_header: u8,
+        section_contents: &mut [u8],
+    ) {
+        // prefix byte layout:
+        // 0-18: Encryption Info DE (header + scheme + salt)
+        // 19-20: Identity DE header
+        // 21-36: Identity DE contents (metadata key)
+        // Encryption Info DE
+        let encryption_info_bytes = EncryptionInfoDataElement::mic(
+            self.salt.as_slice().try_into().expect("Salt should be 16 bytes"),
+        )
+        .serialize();
+        section_contents[0..19].copy_from_slice(&encryption_info_bytes);
+        // Identity DE
+        let identity_header = identity_de_header(self.identity_type, self.metadata_key);
+        section_contents[19..21].copy_from_slice(identity_header.serialize().as_slice());
+        section_contents[21..37].copy_from_slice(&self.metadata_key.0);
+        // DE offset for identity is 1: Encryption Info DE, Identity DE, then other DEs
+        let nonce: AesCtrNonce = self
+            .de_salt(v1_salt::DataElementOffset::from(1))
+            .derive()
+            .expect("AES-CTR nonce is a valid HKDF length");
+        let mut cipher = C::AesCtr128::new(&self.aes_key, NonceAndCounter::from_nonce(nonce));
+        let ciphertext_end = section_contents.len() - SectionMic::CONTENTS_LEN;
+        // encrypt just the part that should be ciphertext: identity DE contents and subsequent DEs
+        cipher.apply_keystream(&mut section_contents[21..ciphertext_end]);
+        // calculate MAC per the spec
+        let mut section_hmac = self.mic_hmac_key.build_hmac();
+        // svc uuid
+        section_hmac.update(NP_SVC_UUID.as_slice());
+        // adv header
+        section_hmac.update(&[adv_header_byte]);
+        // section header
+        section_hmac.update(&[section_header]);
+        // encryption info
+        section_hmac.update(&encryption_info_bytes);
+        // derived salt
+        section_hmac.update(&nonce);
+        // identity header + ciphertext
+        section_hmac.update(&section_contents[19..ciphertext_end]);
+        let mic: [u8; 32] = section_hmac.finalize();
+        // write truncated MIC
+        section_contents[ciphertext_end..].copy_from_slice(&mic[..SectionMic::CONTENTS_LEN]);
+    }
+    type DerivedSalt = DeSalt<C>;
+    fn de_salt(&self, de_offset: DataElementOffset) -> Self::DerivedSalt {
+        DeSalt { salt: V1Salt::from(*self.salt.as_array_ref()), de_offset }
+    }
+}
+
+/// Derived salt for an individual data element.
+pub struct DeSalt<C: CryptoProvider> {
+    salt: V1Salt<C>,
+    de_offset: DataElementOffset,
+}
+
+impl<C: CryptoProvider> DeSalt<C> {
+    /// Derive salt of the requested length.
+    ///
+    /// The length must be a valid HKDF-SHA256 length.
+    pub fn derive<const N: usize>(&self) -> Option<[u8; N]> {
+        self.salt.derive(Some(self.de_offset))
+    }
+}
+
 /// For DE structs that only implement one DE type, rather than multi-type impls.
 pub trait SingleTypeDataElement {
     /// The DE type for the DE.
@@ -766,11 +800,12 @@
 
 fn identity_de_header(
     id_type: EncryptedIdentityDataElementType,
-    metadata_key: &[u8; 16],
+    metadata_key: MetadataKey,
 ) -> DeHeader {
     DeHeader {
         de_type: id_type.type_code(),
         len: metadata_key
+            .0
             .len()
             .try_into()
             .map_err(|_e| DeLengthOutOfRange)
diff --git a/nearby/presence/np_adv/src/extended/serialize/section_tests.rs b/nearby/presence/np_adv/src/extended/serialize/section_tests.rs
index af11076..d418a50 100644
--- a/nearby/presence/np_adv/src/extended/serialize/section_tests.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/section_tests.rs
@@ -15,11 +15,12 @@
 extern crate std;
 
 use super::*;
-use crate::extended::serialize::AddSectionError::MaxSectionCountExceeded;
-use crate::{
-    extended::data_elements::{ContextSyncSeqNumDataElement, GenericDataElement},
-    shared_data::ContextSyncSeqNum,
+use crate::credential::{
+    v1::{SimpleSignedBroadcastCryptoMaterial, V1},
+    SimpleBroadcastCryptoMaterial,
 };
+use crate::extended::data_elements::GenericDataElement;
+use crate::extended::serialize::AddSectionError::MaxSectionCountExceeded;
 use crypto_provider::aes::ctr::AES_CTR_NONCE_LEN;
 use crypto_provider_default::CryptoProviderImpl;
 use np_hkdf::v1_salt::V1Salt;
@@ -32,8 +33,8 @@
 
 #[test]
 fn public_identity_section_empty() {
-    let mut adv_builder = AdvBuilder::new();
-    let section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
 
     assert_eq!(&[1_u8, 0x03], section_builder.into_section().as_slice());
 }
@@ -62,23 +63,26 @@
             .collect::<Vec<_>>();
 
         let metadata_key = rng.gen();
+        let metadata_key = MetadataKey(metadata_key);
         let key_seed = rng.gen();
         let identity_type =
             *EncryptedIdentityDataElementType::iter().collect::<Vec<_>>().choose(&mut rng).unwrap();
         let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 
-        let mut adv_builder = AdvBuilder::new();
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
+        let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
 
         let mut section_builder = adv_builder
-            .section_builder(MicEncrypted::new_random_salt(
+            .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                 &mut crypto_rng,
                 identity_type,
-                &metadata_key,
-                &key_seed_hkdf,
+                &broadcast_cm,
             ))
             .unwrap();
-        let section_salt =
-            V1Salt::<CryptoProviderImpl>::from(*section_builder.identity.salt.as_array_ref());
+        let section_salt = V1Salt::<CryptoProviderImpl>::from(
+            *section_builder.section_encoder.salt.as_array_ref(),
+        );
 
         for de in extra_des.iter() {
             section_builder.add_de(|_| de).unwrap();
@@ -88,9 +92,7 @@
         let section_length = 53
             + extra_des
                 .iter()
-                .map(|de| {
-                    de.de_header().serialize().len() as u8 + de.de_header().len.as_usize() as u8
-                })
+                .map(|de| de.de_header().serialize().len() as u8 + de.de_header().len.as_u8())
                 .sum::<u8>();
 
         let encryption_info = [
@@ -123,14 +125,14 @@
             &np_hkdf::UnsignedSectionKeys::aes_key(&key_seed_hkdf),
             NonceAndCounter::from_nonce(nonce),
         );
-        let mut plaintext = metadata_key.as_slice().to_vec();
+        let mut plaintext = metadata_key.0.as_slice().to_vec();
 
         for de in extra_des {
             plaintext.extend_from_slice(de.de_header().serialize().as_slice());
             de.write_de_contents(&mut plaintext);
         }
 
-        cipher.encrypt(&mut plaintext);
+        cipher.apply_keystream(&mut plaintext);
         let ciphertext = plaintext;
 
         hmac_input.extend_from_slice(&ciphertext);
@@ -162,26 +164,31 @@
     for _ in 0..1_000 {
         let num_des = rng.gen_range(1..=5);
 
-        let metadata_key = rng.gen();
+        let metadata_key = MetadataKey(rng.gen());
         let key_seed = rng.gen();
         let identity_type =
             *EncryptedIdentityDataElementType::iter().collect::<Vec<_>>().choose(&mut rng).unwrap();
         let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 
-        let mut adv_builder = AdvBuilder::new();
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
         let key_pair = KeyPair::generate();
 
+        let broadcast_cm = SimpleSignedBroadcastCryptoMaterial::new(
+            key_seed,
+            metadata_key,
+            key_pair.private_key(),
+        );
+
         let mut section_builder = adv_builder
-            .section_builder(SignedEncrypted::new_random_salt(
+            .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
                 &mut crypto_rng,
                 identity_type,
-                &metadata_key,
-                &key_pair,
-                &key_seed_hkdf,
+                &broadcast_cm,
             ))
             .unwrap();
-        let section_salt =
-            V1Salt::<CryptoProviderImpl>::from(*section_builder.identity.salt.as_array_ref());
+        let section_salt = V1Salt::<CryptoProviderImpl>::from(
+            *section_builder.section_encoder.salt.as_array_ref(),
+        );
 
         let extra_des = (0..num_des)
             .map(|_| {
@@ -201,9 +208,7 @@
             + 64
             + extra_des
                 .iter()
-                .map(|de| {
-                    de.de_header().serialize().len() as u8 + de.de_header().len.as_usize() as u8
-                })
+                .map(|de| de.de_header().serialize().len() as u8 + de.de_header().len.as_u8())
                 .sum::<u8>();
 
         let encryption_info = [
@@ -224,11 +229,10 @@
 
         let nonce = section_salt.derive::<{ AES_CTR_NONCE_LEN }>(Some(1.into())).unwrap();
 
-        let mut plaintext = metadata_key.as_slice().to_vec();
-
+        let mut section_body = Vec::new();
         for de in extra_des {
-            plaintext.extend_from_slice(de.de_header().serialize().as_slice());
-            de.write_de_contents(&mut plaintext);
+            section_body.extend_from_slice(de.de_header().serialize().as_slice());
+            de.write_de_contents(&mut section_body);
         }
 
         let sig_payload = SectionSignaturePayload::from_deserialized_parts(
@@ -237,16 +241,20 @@
             &encryption_info,
             &nonce,
             identity_de_header,
-            &plaintext,
+            metadata_key,
+            &section_body,
         );
 
+        let mut plaintext = metadata_key.0.as_slice().to_vec();
+        plaintext.extend_from_slice(section_body.as_slice());
+
         plaintext.extend_from_slice(&sig_payload.sign(&key_pair).to_bytes());
 
         <CryptoProviderImpl as CryptoProvider>::AesCtr128::new(
             &key_seed_hkdf.extended_signed_section_aes_key(),
             NonceAndCounter::from_nonce(nonce),
         )
-        .encrypt(&mut plaintext);
+        .apply_keystream(&mut plaintext);
         let ciphertext = plaintext;
 
         let mut expected = vec![section_length];
@@ -262,20 +270,23 @@
 fn section_builder_too_full_doesnt_advance_de_index() {
     let mut crypto_rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let key_seed = [22; 32];
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let metadata_key = [33; 16];
+    let metadata_key = MetadataKey([33; 16]);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new_random_salt(
+        .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
             &mut crypto_rng,
             EncryptedIdentityDataElementType::Trusted,
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
-    let salt = V1Salt::<CryptoProviderImpl>::from(*section_builder.identity.salt.as_array_ref());
+    let salt =
+        V1Salt::<CryptoProviderImpl>::from(*section_builder.section_encoder.salt.as_array_ref());
 
     section_builder
         .add_de(|de_salt| DummyDataElement {
@@ -306,7 +317,7 @@
 
     let mut expected = vec![];
     // metadata key
-    expected.extend_from_slice(&metadata_key);
+    expected.extend_from_slice(&metadata_key.0);
     // de header
     expected.extend_from_slice(&[0x80 + 100, 100]);
     // section 0 de 2
@@ -321,7 +332,7 @@
         NonceAndCounter::from_nonce(salt.derive(Some(1.into())).unwrap()),
     );
 
-    cipher.encrypt(&mut expected);
+    cipher.apply_keystream(&mut expected);
 
     let adv_bytes = adv_builder.into_advertisement();
     // ignoring the MIC, etc, since that's tested elsewhere
@@ -332,59 +343,47 @@
 #[test]
 fn section_de_fits_exactly() {
     // leave room for initial filler section's header and the identities
-    for section_contents_capacity in 1..NP_ADV_MAX_SECTION_LEN - 3 {
-        let mut adv_builder = AdvBuilder::new();
+    // for section_contents_capacity in 1..NP_ADV_MAX_SECTION_LEN - 3 {
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
 
-        // fill up space to produce desired capacity
-        let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
-        // leave space for adv header, 2 section headers, the section identities, and desired second section capacity
-        fill_section_builder(
-            BLE_ADV_SVC_CONTENT_LEN - 1 - 2 - 2 - section_contents_capacity,
-            &mut section_builder,
-        );
-        section_builder.add_to_advertisement();
+    // fill up space to produce desired capacity
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
+    // leave space for adv header, 1 section headers, 1 section identity and fill almost full
+    fill_section_builder(BLE_ADV_SVC_CONTENT_LEN - 1 - 1 - 1 - 1, &mut section_builder);
+    assert_eq!(1, section_builder.section.capacity() - section_builder.section.len(), "capacity: ");
 
-        let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    // can't add a 2 byte DE
+    assert_eq!(
+        Err(AddDataElementError::InsufficientSectionSpace),
+        section_builder.add_de_res(|_| GenericDataElement::try_from(1_u32.into(), &[0xFF])),
+        "capacity: "
+    );
 
-        // fill it almost full
-        fill_section_builder(section_contents_capacity - 1, &mut section_builder);
-
-        assert_eq!(
-            1,
-            section_builder.section.capacity() - section_builder.section.len(),
-            "capacity: {section_contents_capacity}"
-        );
-
-        // can't add a 2 byte DE
-        assert_eq!(
-            Err(AddDataElementError::InsufficientSectionSpace),
-            section_builder.add_de_res(|_| GenericDataElement::try_from(1_u32.into(), &[0xFF])),
-            "capacity: {section_contents_capacity}"
-        );
-
-        // can add a 1 byte DE
-        section_builder.add_de_res(|_| GenericDataElement::try_from(1_u32.into(), &[])).unwrap();
-    }
+    // can add a 1 byte DE
+    section_builder.add_de_res(|_| GenericDataElement::try_from(1_u32.into(), &[])).unwrap();
 }
 
 #[test]
 fn section_builder_build_de_error_doesnt_advance_de_index() {
     let mut crypto_rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let key_seed = [22; 32];
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let metadata_key = [33; 16];
+    let metadata_key = MetadataKey([33; 16]);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new_random_salt(
+        .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
             &mut crypto_rng,
             EncryptedIdentityDataElementType::Trusted,
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
-    let salt = V1Salt::<CryptoProviderImpl>::from(*section_builder.identity.salt.as_array_ref());
+    let salt =
+        V1Salt::<CryptoProviderImpl>::from(*section_builder.section_encoder.salt.as_array_ref());
 
     section_builder
         .add_de(|de_salt| DummyDataElement {
@@ -409,7 +408,7 @@
 
     let mut expected = vec![];
     // metadata key
-    expected.extend_from_slice(&metadata_key);
+    expected.extend_from_slice(&metadata_key.0);
     // de header
     expected.extend_from_slice(&[0x80 + 100, 100]);
     // section 0 de 2
@@ -424,7 +423,7 @@
         NonceAndCounter::from_nonce(salt.derive(Some(1.into())).unwrap()),
     );
 
-    cipher.encrypt(&mut expected);
+    cipher.apply_keystream(&mut expected);
 
     let adv_bytes = adv_builder.into_advertisement();
     // ignoring the MIC, etc, since that's tested elsewhere
@@ -436,20 +435,23 @@
 fn add_multiple_de_correct_de_offsets_mic_encrypted_identity() {
     let mut crypto_rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let key_seed = [22; 32];
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let metadata_key = [33; 16];
+    let metadata_key = MetadataKey([33; 16]);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new_random_salt(
+        .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
             &mut crypto_rng,
             EncryptedIdentityDataElementType::Trusted,
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
-    let salt = V1Salt::<CryptoProviderImpl>::from(*section_builder.identity.salt.as_array_ref());
+    let salt =
+        V1Salt::<CryptoProviderImpl>::from(*section_builder.section_encoder.salt.as_array_ref());
 
     section_builder
         .add_de(|de_salt| DummyDataElement {
@@ -468,7 +470,7 @@
 
     let mut expected = vec![];
     // metadata key
-    expected.extend_from_slice(&metadata_key);
+    expected.extend_from_slice(&metadata_key.0);
     // de header
     expected.extend_from_slice(&[0x90, 0x40]);
     // section 0 de 2
@@ -483,7 +485,7 @@
         NonceAndCounter::from_nonce(salt.derive(Some(1.into())).unwrap()),
     );
 
-    cipher.encrypt(&mut expected);
+    cipher.apply_keystream(&mut expected);
 
     let adv_bytes = adv_builder.into_advertisement();
     // ignoring the MIC, etc, since that's tested elsewhere
@@ -495,22 +497,25 @@
 fn add_multiple_de_correct_de_offsets_signature_encrypted_identity() {
     let mut crypto_rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let key_seed = [22; 32];
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let metadata_key = [33; 16];
+    let metadata_key = MetadataKey([33; 16]);
     let key_pair = KeyPair::generate();
+
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
+
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new_random_salt(
+        .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
             &mut crypto_rng,
             EncryptedIdentityDataElementType::Trusted,
-            &metadata_key,
-            &key_pair,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
-    let salt = V1Salt::<CryptoProviderImpl>::from(*section_builder.identity.salt.as_array_ref());
+    let salt =
+        V1Salt::<CryptoProviderImpl>::from(*section_builder.section_encoder.salt.as_array_ref());
 
     section_builder
         .add_de(|de_salt| DummyDataElement {
@@ -529,7 +534,7 @@
 
     let mut expected = vec![];
     // metadata key
-    expected.extend_from_slice(&metadata_key);
+    expected.extend_from_slice(&metadata_key.0);
     // de header
     expected.extend_from_slice(&[0x90, 0x40]);
     // section 0 de 2
@@ -543,7 +548,7 @@
         &key_seed_hkdf.extended_signed_section_aes_key(),
         NonceAndCounter::from_nonce(salt.derive(Some(1.into())).unwrap()),
     )
-    .encrypt(&mut expected);
+    .apply_keystream(&mut expected);
 
     let adv_bytes = adv_builder.into_advertisement();
     // ignoring the signature since that's tested elsewhere
@@ -558,19 +563,20 @@
 fn signature_encrypted_section_de_lengths_allow_room_for_suffix() {
     let mut crypto_rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
 
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let key_seed = [22; 32];
-    let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let metadata_key = [33; 16];
+    let metadata_key = MetadataKey([33; 16]);
     let key_pair = KeyPair::generate();
+
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
+
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new_random_salt(
+        .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
             &mut crypto_rng,
             EncryptedIdentityDataElementType::Trusted,
-            &metadata_key,
-            &key_pair,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -606,35 +612,39 @@
 }
 
 #[test]
-fn serialize_max_number_of_sections() {
-    let mut adv_builder = AdvBuilder::new();
-    for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
-        let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
-        section_builder.add_de(|_| context_sync_de(1)).unwrap();
+fn serialize_max_number_of_public_sections() {
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    for _ in 0..NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT {
+        let mut section_builder =
+            adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
+        section_builder
+            .add_de(|_| DummyDataElement { de_type: 100_u32.into(), data: vec![0; 98] })
+            .unwrap();
         section_builder.add_to_advertisement();
     }
     assert_eq!(
-        adv_builder.section_builder(PublicIdentity::default()).unwrap_err(),
+        adv_builder.section_builder(PublicSectionEncoder::default()).unwrap_err(),
         MaxSectionCountExceeded
     );
 }
 
 fn do_mic_encrypted_identity_fixed_key_material_test<W: WriteDataElement>(extra_des: &[W]) {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let adv_header_byte = 0b00100000;
     let section_salt: V1Salt<CryptoProviderImpl> = [3; 16].into();
     let identity_type = EncryptedIdentityDataElementType::Private;
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 
-    let mut adv_builder = AdvBuilder::new();
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let mut section_builder = adv_builder
-        .section_builder(MicEncrypted::new(
+        .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new(
             identity_type,
             V1Salt::from(*section_salt.as_array_ref()),
-            &metadata_key,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -646,7 +656,7 @@
     let section_length = 53
         + extra_des
             .iter()
-            .map(|de| de.de_header().serialize().len() as u8 + de.de_header().len.as_usize() as u8)
+            .map(|de| de.de_header().serialize().len() as u8 + de.de_header().len.as_u8())
             .sum::<u8>();
 
     let encryption_info = [
@@ -678,14 +688,14 @@
         &np_hkdf::UnsignedSectionKeys::aes_key(&key_seed_hkdf),
         NonceAndCounter::from_nonce(nonce),
     );
-    let mut plaintext = metadata_key.as_slice().to_vec();
+    let mut plaintext = metadata_key.0.as_slice().to_vec();
 
     for de in extra_des {
         plaintext.extend_from_slice(de.de_header().serialize().as_slice());
         de.write_de_contents(&mut plaintext);
     }
 
-    cipher.encrypt(&mut plaintext);
+    cipher.apply_keystream(&mut plaintext);
     let ciphertext = plaintext;
 
     hmac_input.extend_from_slice(&ciphertext);
@@ -704,7 +714,7 @@
 }
 
 fn do_signature_encrypted_identity_fixed_key_material_test<W: WriteDataElement>(extra_des: &[W]) {
-    let metadata_key = [1; 16];
+    let metadata_key = MetadataKey([1; 16]);
     let key_seed = [2; 32];
     let adv_header_byte = 0b00100000;
     let section_salt: V1Salt<CryptoProviderImpl> = [3; 16].into();
@@ -712,15 +722,16 @@
     let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
     let key_pair = KeyPair::generate();
 
-    let mut adv_builder = AdvBuilder::new();
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
+
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new(
+        .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new(
             identity_type,
             V1Salt::from(*section_salt.as_array_ref()),
-            &metadata_key,
-            &key_pair,
-            &key_seed_hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
 
@@ -734,7 +745,7 @@
         + 64
         + extra_des
             .iter()
-            .map(|de| de.de_header().serialize().len() as u8 + de.de_header().len.as_usize() as u8)
+            .map(|de| de.de_header().serialize().len() as u8 + de.de_header().len.as_u8())
             .sum::<u8>();
 
     let encryption_info = [
@@ -751,11 +762,10 @@
         DeHeader { len: 16_u8.try_into().unwrap(), de_type: identity_type.type_code() };
     let identity_de_header: [u8; 2] = identity_de_header.serialize().as_slice().try_into().unwrap();
 
-    let mut plaintext = metadata_key.as_slice().to_vec();
-
+    let mut section_body = Vec::new();
     for de in extra_des {
-        plaintext.extend_from_slice(de.de_header().serialize().as_slice());
-        de.write_de_contents(&mut plaintext);
+        section_body.extend_from_slice(de.de_header().serialize().as_slice());
+        de.write_de_contents(&mut section_body);
     }
 
     let nonce = section_salt.derive(Some(1.into())).unwrap();
@@ -766,16 +776,19 @@
         &encryption_info,
         &nonce,
         identity_de_header,
-        &plaintext,
+        metadata_key,
+        &section_body,
     );
 
+    let mut plaintext = metadata_key.0.as_slice().to_vec();
+    plaintext.extend_from_slice(section_body.as_slice());
     plaintext.extend_from_slice(&sig_payload.sign(&key_pair).to_bytes());
 
     <CryptoProviderImpl as CryptoProvider>::AesCtr128::new(
         &key_seed_hkdf.extended_signed_section_aes_key(),
         NonceAndCounter::from_nonce(nonce),
     )
-    .encrypt(&mut plaintext);
+    .apply_keystream(&mut plaintext);
     let ciphertext = plaintext;
 
     let mut expected = vec![section_length];
@@ -788,7 +801,7 @@
 }
 
 /// Write `section_contents_len` bytes of DE and header into `section_builder`
-pub(crate) fn fill_section_builder<I: SectionIdentity>(
+pub(crate) fn fill_section_builder<I: SectionEncoder>(
     section_contents_len: usize,
     section_builder: &mut SectionBuilder<I>,
 ) {
@@ -844,17 +857,13 @@
     }
 }
 
-fn context_sync_de(num: u8) -> ContextSyncSeqNumDataElement {
-    ContextSyncSeqNumDataElement::from(ContextSyncSeqNum::try_from(num).unwrap())
-}
-
 pub(crate) trait SectionBuilderExt {
     fn into_section(self) -> EncodedSection;
 }
 
-impl<'a, I: SectionIdentity> SectionBuilderExt for SectionBuilder<'a, I> {
+impl<'a, I: SectionEncoder> SectionBuilderExt for SectionBuilder<'a, I> {
     /// Convenience method for tests
     fn into_section(self) -> EncodedSection {
-        Self::build_section(self.section.into_inner(), self.identity, self.adv_builder)
+        Self::build_section(self.section.into_inner(), self.section_encoder, self.adv_builder)
     }
 }
diff --git a/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs b/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs
index 9f7aa5c..24e6046 100644
--- a/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs
+++ b/nearby/presence/np_adv/src/extended/serialize/test_vectors.rs
@@ -14,12 +14,15 @@
 
 extern crate std;
 
+use crate::extended::serialize::AdvertisementType;
 use crate::{
+    credential::{v1::V1, SimpleBroadcastCryptoMaterial},
     de_type::EncryptedIdentityDataElementType,
     extended::serialize::{
         section_tests::{DummyDataElement, SectionBuilderExt},
-        AdvBuilder, MicEncrypted,
+        AdvBuilder, MicEncryptedSectionEncoder,
     },
+    MetadataKey,
 };
 use anyhow::anyhow;
 use crypto_provider::{aes::ctr::AES_CTR_NONCE_LEN, aes::AesKey};
@@ -49,7 +52,7 @@
     for tc in test_cases {
         {
             let key_seed = extract_key_array::<32>(&tc, "key_seed");
-            let metadata_key = extract_key_array::<16>(&tc, "metadata_key");
+            let metadata_key = MetadataKey(extract_key_array::<16>(&tc, "metadata_key"));
             let adv_header_byte = extract_key_array::<1>(&tc, "adv_header_byte")[0];
             let section_salt = v1_salt::V1Salt::<CryptoProviderImpl>::from(
                 extract_key_array::<16>(&tc, "section_salt"),
@@ -65,7 +68,7 @@
                 })
                 .collect::<Vec<_>>();
 
-            let hkdf = np_hkdf::NpKeySeedHkdf::new(&key_seed);
+            let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 
             assert_eq!(
                 extract_key_array::<16>(&tc, "aes_key").as_slice(),
@@ -80,16 +83,17 @@
                 section_salt.derive::<{ AES_CTR_NONCE_LEN }>(Some(1.into())).unwrap()
             );
 
+            let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
             // make an adv builder in the configuration we need
-            let mut adv_builder = AdvBuilder::new();
+            let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
             assert_eq!(adv_header_byte, adv_builder.header_byte());
 
             let mut section_builder = adv_builder
-                .section_builder(MicEncrypted::new(
+                .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new(
                     identity_type,
                     section_salt,
-                    &metadata_key,
-                    &hkdf,
+                    &broadcast_cm,
                 ))
                 .unwrap();
 
@@ -127,7 +131,7 @@
             })
             .collect::<Vec<_>>();
 
-        let metadata_key = rng.gen();
+        let metadata_key = MetadataKey(rng.gen());
         let key_seed = rng.gen();
         let adv_header_byte = 0b00100000;
         let identity_type =
@@ -135,15 +139,16 @@
 
         let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 
-        let mut adv_builder = AdvBuilder::new();
+        let broadcast_cm = SimpleBroadcastCryptoMaterial::<V1>::new(key_seed, metadata_key);
+
+        let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
 
         let section_salt = v1_salt::V1Salt::<CryptoProviderImpl>::from(rng.gen::<[u8; 16]>());
         let mut section_builder = adv_builder
-            .section_builder(MicEncrypted::new(
+            .section_builder(MicEncryptedSectionEncoder::<CryptoProviderImpl>::new(
                 identity_type,
                 V1Salt::from(*section_salt.as_array_ref()),
-                &metadata_key,
-                &key_seed_hkdf,
+                &broadcast_cm,
             ))
             .unwrap();
 
@@ -156,7 +161,7 @@
         array
             .push(json!({
                 "key_seed": hex::encode_upper(key_seed),
-                "metadata_key": hex::encode_upper(metadata_key),
+                "metadata_key": hex::encode_upper(metadata_key.0),
                 "adv_header_byte": hex::encode_upper([adv_header_byte]),
                 "section_salt": hex::encode_upper(section_salt.as_slice()),
                 "identity_type": identity_type_label(identity_type),
diff --git a/nearby/presence/np_adv/src/filter/mod.rs b/nearby/presence/np_adv/src/filter/mod.rs
new file mode 100644
index 0000000..cb020cf
--- /dev/null
+++ b/nearby/presence/np_adv/src/filter/mod.rs
@@ -0,0 +1,291 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Provides filtering APIs to determine whether or not a buffer of bytes represents
+//! a valid Nearby Presence advertisement and if so which was its corresponding identity
+//! it matched with. Used as a first pass option to quickly check if a buffer should
+//! further processed.
+use crate::credential::MatchedCredential;
+use crate::legacy::deserialize::DecryptedAdvContents;
+use crate::{
+    credential::{book::CredentialBook, v0::V0DiscoveryCryptoMaterial},
+    legacy,
+    legacy::{
+        actions,
+        actions::ActionsDataElement,
+        deserialize::{
+            DecryptError, EncryptedAdvContents, IntermediateAdvContents, PlainDataElement,
+        },
+        PacketFlavor,
+    },
+    parse_adv_header, AdvHeader, V1Header,
+};
+use array_view::ArrayView;
+use core::fmt::Debug;
+use crypto_provider::CryptoProvider;
+
+#[cfg(test)]
+mod tests;
+
+/// The options to filter for in an advertisement payload.
+pub enum FilterOptions {
+    /// Filter criteria for matching against only v0 advertisements
+    V0FilterOptions(V0Filter),
+    /// Filter criteria for matching against only v1 advertisements
+    V1FilterOptions(V1Filter),
+    /// Criteria to filter for in both v0 and v1 advertisements
+    Either(V0Filter, V1Filter),
+}
+
+/// Error returned when no advertisement matching the criteria was found.
+#[derive(PartialEq, Debug)]
+pub struct NoMatch;
+
+/// Error returned if the provided slice is of invalid length
+#[derive(PartialEq, Debug)]
+pub struct InvalidLength;
+
+/// The criteria of what to filter for in a V0 advertisement.
+pub struct V0Filter {
+    identity: IdentityFilterType,
+    data_elements: V0DataElementsFilter,
+}
+
+/// The type of identity to filter for.
+pub enum IdentityFilterType {
+    /// Filter only on public identity advertisements
+    Public,
+    /// Filter only on private identity advertisements
+    Private,
+    /// Don't filter on any specific identity type
+    Any,
+}
+
+/// Returned by the filter API indicating the status and in the case of private identity the key seed
+/// which matched the decrypted advertisement
+#[derive(Debug, PartialEq)]
+pub enum FilterResult<M: MatchedCredential> {
+    /// A public identity advertisement matching the filter criteria was detected.
+    Public,
+    /// A Private identity advertisement matching the filter criteria was detected and decrypted
+    /// with the contained `KeySeed`.
+    Private(M),
+}
+
+/// The criteria of what data elements to filter for in a V0 advertisement.
+pub struct V0DataElementsFilter {
+    contains_tx_power: Option<bool>,
+    actions_filter: Option<V0ActionsFilter>,
+}
+
+/// The total number of unique boolean action types
+const NUM_ACTIONS: usize = 7;
+
+/// Specify which specific actions bits to filter on, will filter on if any of the specified
+/// actions are matched
+//TODO: do we need a more specific filter criteria, eg: only return if `(A AND B) || C` are found?
+// Potentially this could be pulled out more generally into a V0Filter trait which has many different
+// implementations.
+pub struct V0ActionsFilter {
+    actions: array_view::ArrayView<Option<actions::ActionType>, NUM_ACTIONS>,
+}
+
+impl FilterOptions {
+    /// WARNING: This does not perform signature verification on the advertisement, even if it detects
+    /// a valid advertisement it does not verify it. Verification must occur before further processing
+    /// of the adv.
+    ///
+    /// Returns `Ok` if a advertisement was detected which matched the filter criteria, and `Err` otherwise.
+    pub fn match_advertisement<'a, B, P>(
+        &self,
+        cred_book: &'a B,
+        adv: &[u8],
+    ) -> Result<FilterResult<B::Matched>, NoMatch>
+    where
+        B: CredentialBook<'a>,
+        P: CryptoProvider,
+    {
+        parse_adv_header(adv)
+            .map(|(remaining, header)| match header {
+                AdvHeader::V0 => {
+                    let filter = match self {
+                        FilterOptions::V0FilterOptions(filter) => filter,
+                        FilterOptions::Either(filter, _) => filter,
+                        _ => return Err(NoMatch),
+                    };
+                    filter.match_v0_adv::<B, P>(cred_book, remaining)
+                }
+                AdvHeader::V1(header) => {
+                    let filter = match self {
+                        FilterOptions::V1FilterOptions(filter) => filter,
+                        FilterOptions::Either(_, filter) => filter,
+                        _ => return Err(NoMatch),
+                    };
+                    filter.match_v1_adv::<B, P>(cred_book, remaining, header)
+                }
+            })
+            .map_err(|_| NoMatch)?
+    }
+}
+
+impl V0Filter {
+    /// Filter for the provided criteria returning success if the advertisement bytes successfully
+    /// match the filter criteria
+    fn match_v0_adv<'a, B, P>(
+        &self,
+        cred_book: &'a B,
+        remaining: &[u8],
+    ) -> Result<FilterResult<B::Matched>, NoMatch>
+    where
+        B: CredentialBook<'a>,
+        P: CryptoProvider,
+    {
+        let contents =
+            legacy::deserialize::deserialize_adv_contents::<P>(remaining).map_err(|_| NoMatch)?;
+        match contents {
+            IntermediateAdvContents::Plaintext(p) => match self.identity {
+                IdentityFilterType::Public | IdentityFilterType::Any => self
+                    .data_elements
+                    .match_v0_legible_adv(p.data_elements())
+                    .map(|()| FilterResult::Public),
+                _ => Err(NoMatch),
+            },
+            IntermediateAdvContents::Ciphertext(c) => match self.identity {
+                IdentityFilterType::Private | IdentityFilterType::Any => {
+                    let (legible_adv, m) = try_decrypt_and_match::<B, P>(cred_book.v0_iter(), &c)?;
+                    self.data_elements
+                        .match_v0_legible_adv(legible_adv.data_elements())
+                        .map(|()| FilterResult::Private(m))
+                }
+                _ => Err(NoMatch),
+            },
+        }
+    }
+}
+
+fn try_decrypt_and_match<'a, B, P>(
+    v0_creds: B::V0Iterator,
+    adv: &EncryptedAdvContents,
+) -> Result<(DecryptedAdvContents, B::Matched), NoMatch>
+where
+    B: CredentialBook<'a>,
+    P: CryptoProvider,
+{
+    for (crypto_material, m) in v0_creds {
+        let ldt = crypto_material.ldt_adv_cipher::<P>();
+        match adv.try_decrypt(&ldt) {
+            Ok(c) => return Ok((c, m)),
+            Err(e) => match e {
+                DecryptError::DecryptOrVerifyError => continue,
+                DecryptError::DeserializeError(_) => {
+                    return Err(NoMatch);
+                }
+            },
+        }
+    }
+    Err(NoMatch)
+}
+
+impl V0DataElementsFilter {
+    /// A legible adv is either plaintext to begin with, or decrypted contents from an encrypted adv
+    fn match_v0_legible_adv<'a, F: PacketFlavor + 'a>(
+        &self,
+        mut data_elements: impl ExactSizeIterator<Item = &'a PlainDataElement<F>>,
+    ) -> Result<(), NoMatch> {
+        match &self.contains_tx_power {
+            None => Ok(()),
+            Some(c) => {
+                if c == &data_elements.any(|de| matches!(de, PlainDataElement::TxPower(_))) {
+                    Ok(())
+                } else {
+                    Err(NoMatch)
+                }
+            }
+        }?;
+
+        match &self.actions_filter {
+            None => Ok(()),
+            Some(filter) => {
+                // find if an actions DE exists, if so match on the provided action filter
+                let actions = data_elements.find_map(|de| match de {
+                    PlainDataElement::Actions(actions) => Some(actions),
+                    _ => None,
+                });
+                if let Some(actions) = actions {
+                    filter.match_v0_actions(actions)
+                } else {
+                    return Err(NoMatch);
+                }
+            }
+        }?;
+
+        Ok(())
+    }
+}
+
+impl V0ActionsFilter {
+    /// Creates a new filter from a slice of Actions to look for. Will succeed if any of the provided
+    /// actions are found. Maximum length of `actions` is 7, as there are 7 unique actions possible to
+    /// filter on for V0.
+    pub fn new_from_slice(actions: &[actions::ActionType]) -> Result<Self, InvalidLength> {
+        if actions.len() > NUM_ACTIONS {
+            return Err(InvalidLength);
+        }
+        let mut filter_actions = [None; NUM_ACTIONS];
+        for (i, action) in actions.iter().map(|action| Some(*action)).enumerate() {
+            // i will always be in bounds because the length is checked above
+            filter_actions[i] = action;
+        }
+        Ok(ArrayView::try_from_array(filter_actions, actions.len())
+            .map(|actions| Self { actions })
+            .expect("Length checked above, so this can't happen"))
+    }
+
+    fn match_v0_actions<F: PacketFlavor>(
+        &self,
+        actions: &ActionsDataElement<F>,
+    ) -> Result<(), NoMatch> {
+        for action in self.actions.as_slice().iter() {
+            if actions
+                .action
+                .has_action(&action.expect("This will always contain Some"))
+                .unwrap_or(false)
+            {
+                return Ok(());
+            }
+        }
+        Err(NoMatch)
+    }
+}
+
+/// The criteria of what to filter for in a V1 advertisement.
+pub struct V1Filter;
+
+impl V1Filter {
+    /// Filter for the provided criteria returning success if the advertisement bytes successfully
+    /// match the filter criteria
+    #[allow(clippy::extra_unused_type_parameters)]
+    fn match_v1_adv<'a, B, P>(
+        &self,
+        _cred_book: &'a B,
+        _remaining: &[u8],
+        _header: V1Header,
+    ) -> Result<FilterResult<B::Matched>, NoMatch>
+    where
+        B: CredentialBook<'a>,
+        P: CryptoProvider,
+    {
+        todo!()
+    }
+}
diff --git a/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs b/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs
new file mode 100644
index 0000000..946c969
--- /dev/null
+++ b/nearby/presence/np_adv/src/filter/tests/actions_filter_tests.rs
@@ -0,0 +1,136 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::super::*;
+use crate::legacy::actions::{ActionBits, InstantTethering, NearbyShare};
+use crate::legacy::{Ciphertext, Plaintext};
+
+#[test]
+fn new_v0_actions_invalid_length() {
+    let actions = [actions::ActionType::ActiveUnlock; 8];
+    let result = V0ActionsFilter::new_from_slice(&actions);
+    assert!(result.is_err());
+    assert_eq!(result.err().unwrap(), InvalidLength)
+}
+
+#[test]
+fn new_v0_actions() {
+    let actions = [actions::ActionType::ActiveUnlock; 7];
+    let result = V0ActionsFilter::new_from_slice(&actions);
+    assert!(result.is_ok());
+}
+
+#[test]
+fn new_v0_actions_empty_slice() {
+    let result = V0ActionsFilter::new_from_slice(&[]);
+    assert!(result.is_ok());
+}
+
+#[test]
+fn test_actions_filter_single_action_not_present() {
+    // default is all 0 bits
+    let action_bits = ActionBits::<Plaintext>::default();
+
+    let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
+        .expect("1 is a valid length");
+
+    assert_eq!(filter.match_v0_actions(&action_bits.into()), Err(NoMatch))
+}
+
+#[test]
+fn test_actions_filter_all_actions_not_present() {
+    // default is all 0 bits
+    let action_bits = ActionBits::<Plaintext>::default();
+
+    let filter = V0ActionsFilter::new_from_slice(&[
+        actions::ActionType::ActiveUnlock,
+        actions::ActionType::NearbyShare,
+        actions::ActionType::InstantTethering,
+        actions::ActionType::PhoneHub,
+        actions::ActionType::Finder,
+        actions::ActionType::FastPairSass,
+        actions::ActionType::PresenceManager,
+    ])
+    .expect("7 is a valid length");
+
+    assert_eq!(filter.match_v0_actions(&action_bits.into()), Err(NoMatch))
+}
+
+#[test]
+fn test_actions_filter_single_action_present() {
+    // default is all 0 bits
+    let mut action_bits = ActionBits::<Plaintext>::default();
+    action_bits.set_action(NearbyShare::from(true));
+
+    let filter = V0ActionsFilter::new_from_slice(&[
+        actions::ActionType::ActiveUnlock,
+        actions::ActionType::NearbyShare,
+        actions::ActionType::InstantTethering,
+        actions::ActionType::PhoneHub,
+        actions::ActionType::Finder,
+        actions::ActionType::FastPairSass,
+        actions::ActionType::PresenceManager,
+    ])
+    .expect("7 is a valid length");
+
+    assert_eq!(filter.match_v0_actions(&action_bits.into()), Ok(()))
+}
+
+#[test]
+fn test_actions_filter_desired_action_not_present() {
+    // default is all 0 bits
+    let mut action_bits = ActionBits::<Plaintext>::default();
+    action_bits.set_action(NearbyShare::from(true));
+
+    let filter = V0ActionsFilter::new_from_slice(&[
+        actions::ActionType::ActiveUnlock,
+        actions::ActionType::InstantTethering,
+        actions::ActionType::PhoneHub,
+        actions::ActionType::Finder,
+        actions::ActionType::FastPairSass,
+        actions::ActionType::PresenceManager,
+    ])
+    .expect("6 is a valid length");
+
+    assert_eq!(filter.match_v0_actions(&action_bits.into()), Err(NoMatch))
+}
+
+#[test]
+fn test_multiple_actions_set() {
+    // default is all 0 bits
+    let mut action_bits = ActionBits::<Ciphertext>::default();
+    action_bits.set_action(NearbyShare::from(true));
+    action_bits.set_action(InstantTethering::from(true));
+
+    let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::InstantTethering])
+        .expect("1 is a valid length");
+
+    assert_eq!(filter.match_v0_actions(&action_bits.into()), Ok(()))
+}
+
+#[test]
+fn test_multiple_actions_set_both_present() {
+    // default is all 0 bits
+    let mut action_bits = ActionBits::<Ciphertext>::default();
+    action_bits.set_action(NearbyShare::from(true));
+    action_bits.set_action(InstantTethering::from(true));
+
+    let filter = V0ActionsFilter::new_from_slice(&[
+        actions::ActionType::InstantTethering,
+        actions::ActionType::NearbyShare,
+    ])
+    .expect("7 is a valid length");
+
+    assert_eq!(filter.match_v0_actions(&action_bits.into()), Ok(()))
+}
diff --git a/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs b/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs
new file mode 100644
index 0000000..cb7c067
--- /dev/null
+++ b/nearby/presence/np_adv/src/filter/tests/data_elements_filter_tests.rs
@@ -0,0 +1,106 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::super::*;
+use crate::legacy::actions::{ActionBits, ActiveUnlock};
+use crate::legacy::data_elements::TxPowerDataElement;
+use crate::legacy::{Ciphertext, Plaintext};
+use crate::shared_data::TxPower;
+
+#[test]
+fn match_contains_tx_power() {
+    let filter = V0DataElementsFilter { contains_tx_power: Some(true), actions_filter: None };
+
+    let tx_power = TxPower::try_from(5).expect("within range");
+    let tx_power_de = TxPowerDataElement::from(tx_power);
+    let result =
+        filter.match_v0_legible_adv([PlainDataElement::<Ciphertext>::TxPower(tx_power_de)].iter());
+    assert_eq!(result, Ok(()))
+}
+
+#[test]
+fn match_not_contains_tx_power() {
+    let filter = V0DataElementsFilter { contains_tx_power: Some(true), actions_filter: None };
+    let result = filter.match_v0_legible_adv::<Plaintext>([].iter());
+    assert_eq!(result, Err(NoMatch))
+}
+
+#[test]
+fn match_not_contains_actions() {
+    let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
+        .expect("1 is a valid length");
+    let filter = V0DataElementsFilter { contains_tx_power: None, actions_filter: Some(filter) };
+    let tx_power = TxPower::try_from(5).expect("within range");
+    let tx_power_de = TxPowerDataElement::from(tx_power);
+    let result =
+        filter.match_v0_legible_adv([PlainDataElement::<Ciphertext>::TxPower(tx_power_de)].iter());
+    assert_eq!(result, Err(NoMatch))
+}
+
+#[test]
+fn match_contains_actions() {
+    let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
+        .expect("1 is a valid length");
+    let filter = V0DataElementsFilter { contains_tx_power: None, actions_filter: Some(filter) };
+    let tx_power = TxPower::try_from(5).expect("within range");
+    let tx_power_de = TxPowerDataElement::from(tx_power);
+
+    let mut action_bits = ActionBits::<Ciphertext>::default();
+    action_bits.set_action(ActiveUnlock::from(true));
+
+    let result = filter.match_v0_legible_adv(
+        [
+            PlainDataElement::<Ciphertext>::TxPower(tx_power_de),
+            PlainDataElement::Actions(action_bits.into()),
+        ]
+        .iter(),
+    );
+    assert_eq!(result, Ok(()))
+}
+
+#[test]
+fn match_contains_both() {
+    let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
+        .expect("1 is a valid length");
+    let filter =
+        V0DataElementsFilter { contains_tx_power: Some(true), actions_filter: Some(filter) };
+    let tx_power = TxPower::try_from(5).expect("within range");
+    let tx_power_de = TxPowerDataElement::from(tx_power);
+
+    let mut action_bits = ActionBits::<Ciphertext>::default();
+    action_bits.set_action(ActiveUnlock::from(true));
+
+    let result = filter.match_v0_legible_adv(
+        [
+            PlainDataElement::<Ciphertext>::TxPower(tx_power_de),
+            PlainDataElement::Actions(action_bits.into()),
+        ]
+        .iter(),
+    );
+    assert_eq!(result, Ok(()))
+}
+
+#[test]
+fn match_contains_either() {
+    let filter = V0ActionsFilter::new_from_slice(&[actions::ActionType::ActiveUnlock; 1])
+        .expect("1 is a valid length");
+    let filter =
+        V0DataElementsFilter { contains_tx_power: Some(true), actions_filter: Some(filter) };
+    let tx_power = TxPower::try_from(5).expect("within range");
+    let tx_power_de = TxPowerDataElement::from(tx_power);
+
+    let result =
+        filter.match_v0_legible_adv([PlainDataElement::<Ciphertext>::TxPower(tx_power_de)].iter());
+    assert_eq!(result, Err(NoMatch))
+}
diff --git a/nearby/presence/np_adv/src/filter/tests/mod.rs b/nearby/presence/np_adv/src/filter/tests/mod.rs
new file mode 100644
index 0000000..29d3e7c
--- /dev/null
+++ b/nearby/presence/np_adv/src/filter/tests/mod.rs
@@ -0,0 +1,107 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::credential::book::CredentialBookBuilder;
+use crate::credential::KeySeedMatchedCredential;
+use crate::filter::IdentityFilterType::Any;
+use crate::filter::{
+    FilterOptions, FilterResult, NoMatch, V0DataElementsFilter, V0Filter, V1Filter,
+};
+use crypto_provider_default::CryptoProviderImpl;
+
+mod actions_filter_tests;
+mod data_elements_filter_tests;
+mod v0_filter_tests;
+
+#[test]
+fn top_level_match_v0_adv() {
+    let v0_filter = V0Filter {
+        identity: Any,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let empty_cred_book =
+        CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+            0,
+            0,
+            CryptoProviderImpl,
+        >(&[], &[]);
+
+    let filter = FilterOptions::V0FilterOptions(v0_filter);
+    let result = filter.match_advertisement::<_, CryptoProviderImpl>(
+        &empty_cred_book,
+        &[
+            0x0,  // adv header
+            0x03, // public DE
+            0x16, 0x00, // actions
+        ],
+    );
+
+    assert_eq!(Ok(FilterResult::Public), result);
+}
+
+#[test]
+fn top_level_match_v0_adv_either() {
+    let v0_filter = V0Filter {
+        identity: Any,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let empty_cred_book =
+        CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+            0,
+            0,
+            CryptoProviderImpl,
+        >(&[], &[]);
+
+    let filter = FilterOptions::Either(v0_filter, V1Filter {});
+    let result = filter.match_advertisement::<_, CryptoProviderImpl>(
+        &empty_cred_book,
+        &[
+            0x0,  // adv header
+            0x03, // public DE
+            0x16, 0x00, // actions
+        ],
+    );
+
+    assert_eq!(Ok(FilterResult::Public), result);
+}
+
+#[test]
+fn top_level_no_match_v0_adv() {
+    let v0_filter = V0Filter {
+        identity: Any,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let empty_cred_book =
+        CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+            0,
+            0,
+            CryptoProviderImpl,
+        >(&[], &[]);
+
+    let filter = FilterOptions::V0FilterOptions(v0_filter);
+    let result = filter.match_advertisement::<_, CryptoProviderImpl>(
+        &empty_cred_book,
+        &[
+            0x20, // V1 Advertisement header
+            0x03, // Section Header
+            0x03, // Public Identity DE header
+            0x15, 0x03, // Length 1 Tx Power DE with value 3
+        ],
+    );
+
+    assert_eq!(Err(NoMatch), result);
+}
diff --git a/nearby/presence/np_adv/src/filter/tests/v0_filter_tests.rs b/nearby/presence/np_adv/src/filter/tests/v0_filter_tests.rs
new file mode 100644
index 0000000..971a39e
--- /dev/null
+++ b/nearby/presence/np_adv/src/filter/tests/v0_filter_tests.rs
@@ -0,0 +1,221 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::super::*;
+use crate::credential::book::CredentialBookBuilder;
+use crate::credential::v0::{V0DiscoveryCredential, V0};
+use crate::credential::{
+    KeySeedMatchedCredential, MatchableCredential, ReferencedMatchedCredential,
+};
+use crate::filter::IdentityFilterType::{Any, Private, Public};
+use crypto_provider_default::CryptoProviderImpl;
+use ldt_np_adv::NP_LEGACY_METADATA_KEY_LEN;
+
+const METADATA_KEY: [u8; NP_LEGACY_METADATA_KEY_LEN] = [0x33; NP_LEGACY_METADATA_KEY_LEN];
+const KEY_SEED: [u8; 32] = [0x11_u8; 32];
+const PRIVATE_IDENTITY_V0_ADV_CONTENTS: [u8; 19] = [
+    0x21, // private DE
+    0x22, 0x22, // salt
+    // ciphertext
+    0x85, 0xBF, 0xA8, 0x83, 0x58, 0x7C, 0x50, 0xCF, 0x98, 0x38, 0xA7, 0x8A, 0xC0, 0x1C, 0x96, 0xF9,
+];
+
+const PUBLIC_IDENTITY_V0_ADV_CONTENTS: [u8; 3] = [
+    0x03, // public DE
+    0x16, 0x00, // actions
+];
+
+#[test]
+fn test_contains_public_identity() {
+    let filter = V0Filter {
+        identity: Public,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PUBLIC_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Ok(FilterResult::Public));
+}
+
+#[test]
+fn test_not_contains_private() {
+    let filter = V0Filter {
+        identity: Private,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PUBLIC_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Err(NoMatch));
+}
+
+#[test]
+fn test_contains_any_public() {
+    let filter = V0Filter {
+        identity: Any,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PUBLIC_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Ok(FilterResult::Public));
+}
+
+#[test]
+fn test_not_contains_public_identity() {
+    let filter = V0Filter {
+        identity: Public,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PRIVATE_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Err(NoMatch));
+}
+
+#[test]
+fn test_contains_private_identity() {
+    let filter = V0Filter {
+        identity: Private,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&KEY_SEED);
+    let metadata_key_hmac: [u8; 32] =
+        hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&METADATA_KEY);
+    let discovery_credential = V0DiscoveryCredential::new(KEY_SEED, metadata_key_hmac);
+    let match_data: KeySeedMatchedCredential = KEY_SEED.into();
+    let v0_creds: [MatchableCredential<V0, KeySeedMatchedCredential>; 1] =
+        [MatchableCredential { discovery_credential, match_data: match_data.clone() }];
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&v0_creds, &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PRIVATE_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Ok(FilterResult::Private(ReferencedMatchedCredential::from(&match_data))));
+}
+
+#[test]
+fn test_contains_any_private_identity() {
+    let filter = V0Filter {
+        identity: Any,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&KEY_SEED);
+    let metadata_key_hmac: [u8; 32] =
+        hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&METADATA_KEY);
+    let discovery_credential = V0DiscoveryCredential::new(KEY_SEED, metadata_key_hmac);
+    let match_data: KeySeedMatchedCredential = KEY_SEED.into();
+    let v0_creds: [MatchableCredential<V0, KeySeedMatchedCredential>; 1] =
+        [MatchableCredential { discovery_credential, match_data: match_data.clone() }];
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&v0_creds, &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PRIVATE_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Ok(FilterResult::Private(ReferencedMatchedCredential::from(&match_data))));
+}
+
+#[test]
+fn test_contains_private_identity_no_matching_credential() {
+    let filter = V0Filter {
+        identity: Private,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let mut key_seed = [0u8; 32];
+    key_seed.copy_from_slice(&KEY_SEED);
+    // change one byte
+    key_seed[0] = 0x00;
+
+    let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
+    let metadata_key_hmac: [u8; 32] =
+        hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&METADATA_KEY);
+    let discovery_credential = V0DiscoveryCredential::new(key_seed, metadata_key_hmac);
+    let v0_creds: [MatchableCredential<V0, KeySeedMatchedCredential>; 1] =
+        [MatchableCredential { discovery_credential, match_data: KEY_SEED.into() }];
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&v0_creds, &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PRIVATE_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Err(NoMatch));
+}
+
+#[test]
+fn test_contains_private_identity_invalid_hmac_match() {
+    let filter = V0Filter {
+        identity: Private,
+        data_elements: V0DataElementsFilter { contains_tx_power: None, actions_filter: None },
+    };
+
+    let discovery_credential = V0DiscoveryCredential::new(KEY_SEED, [0u8; 32]);
+    let v0_creds: [MatchableCredential<V0, KeySeedMatchedCredential>; 1] =
+        [MatchableCredential { discovery_credential, match_data: KEY_SEED.into() }];
+
+    let cred_book = CredentialBookBuilder::<KeySeedMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&v0_creds, &[]);
+
+    let result =
+        filter.match_v0_adv::<_, CryptoProviderImpl>(&cred_book, &PRIVATE_IDENTITY_V0_ADV_CONTENTS);
+
+    assert_eq!(result, Err(NoMatch));
+}
diff --git a/nearby/presence/np_adv/src/legacy/actions/macros.rs b/nearby/presence/np_adv/src/legacy/actions/macros.rs
index ecafeb3..8208a57 100644
--- a/nearby/presence/np_adv/src/legacy/actions/macros.rs
+++ b/nearby/presence/np_adv/src/legacy/actions/macros.rs
@@ -44,7 +44,7 @@
     };
 }
 
-/// Create a struct w/ From<bool> and ActionElement impls.
+/// Create a struct w/ `From<bool>` and [`ActionElement`](super::ActionElement) impls.
 /// Use `plaintext_only`, `ciphertext_only`, or `plaintext_and_ciphertext` to create appropriate
 /// impls.
 macro_rules! boolean_element {
@@ -108,7 +108,7 @@
     };
 }
 
-/// Create a [ToActionElement<Encrypted>] impl with the given index and length 1.
+/// Create a [`ToActionElement<Encrypted>`](super::ToActionElement) impl with the given index and length 1.
 macro_rules! boolean_element_to_encrypted_element {
     ( $type_name:ident) => {
         impl $crate::legacy::actions::ToActionElement<$crate::legacy::Ciphertext> for $type_name {
@@ -119,7 +119,7 @@
     };
 }
 
-/// Create a [ToActionElement<Plaintext>] impl with the given index and length 1.
+/// Create a [`ToActionElement<Plaintext>`](super::ToActionElement) impl with the given index and length 1.
 macro_rules! boolean_element_to_plaintext_element {
     ( $type_name:ident) => {
         impl $crate::legacy::actions::ToActionElement<$crate::legacy::Plaintext> for $type_name {
diff --git a/nearby/presence/np_adv/src/legacy/actions/mod.rs b/nearby/presence/np_adv/src/legacy/actions/mod.rs
index 353848e..90a0186 100644
--- a/nearby/presence/np_adv/src/legacy/actions/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/actions/mod.rs
@@ -42,27 +42,8 @@
 /// of 3 bytes occupied by the DE payload.
 #[derive(Debug, PartialEq, Eq)]
 pub struct ActionsDataElement<F: PacketFlavor> {
-    action: ActionBits<F>,
-}
-
-impl<F: PacketFlavor> ActionsDataElement<F> {
-    /// Returns the actions bits as a u32. The upper limit of an actions field is 3 bytes,
-    /// so the last bytes of this u32 will always be 0
-    pub fn as_u32(self) -> u32 {
-        self.action.bits
-    }
-
-    /// Return whether a boolean action type is set in this data element, or `None` if the given
-    /// action type does not represent a boolean.
-    pub fn has_action(&self, action_type: &ActionType) -> Option<bool> {
-        (action_type.bits_len() == 1).then_some(self.action.bits_for_type(action_type) != 0)
-    }
-
-    /// Return the context sync sequence number.
-    pub fn context_sync_seq_num(&self) -> ContextSyncSeqNum {
-        ContextSyncSeqNum::try_from(self.action.bits_for_type(&ActionType::ContextSyncSeqNum) as u8)
-            .expect("Masking with ActionType::ContextSyncSeqNum should always be in range")
-    }
+    /// The action bits
+    pub action: ActionBits<F>,
 }
 
 pub(crate) const ACTIONS_MAX_LEN: usize = 3;
@@ -172,6 +153,26 @@
     flavor: marker::PhantomData<F>,
 }
 
+impl<F: PacketFlavor> ActionBits<F> {
+    /// Returns the actions bits as a u32. The upper limit of an actions field is 3 bytes,
+    /// so the last bytes of this u32 will always be 0
+    pub fn as_u32(self) -> u32 {
+        self.bits
+    }
+
+    /// Return whether a boolean action type is set in this data element, or `None` if the given
+    /// action type does not represent a boolean.
+    pub fn has_action(&self, action_type: &ActionType) -> Option<bool> {
+        (action_type.bits_len() == 1).then_some(self.bits_for_type(action_type) != 0)
+    }
+
+    /// Return the context sync sequence number.
+    pub fn context_sync_seq_num(&self) -> ContextSyncSeqNum {
+        ContextSyncSeqNum::try_from(self.bits_for_type(&ActionType::ContextSyncSeqNum) as u8)
+            .expect("Masking with ActionType::ContextSyncSeqNum should always be in range")
+    }
+}
+
 impl<F: PacketFlavor> Default for ActionBits<F> {
     fn default() -> Self {
         ActionBits {
diff --git a/nearby/presence/np_adv/src/legacy/actions/tests.rs b/nearby/presence/np_adv/src/legacy/actions/tests.rs
index 2e3fda0..86b7a08 100644
--- a/nearby/presence/np_adv/src/legacy/actions/tests.rs
+++ b/nearby/presence/np_adv/src/legacy/actions/tests.rs
@@ -294,14 +294,14 @@
     let mut action_bits = ActionBits::<Plaintext>::default();
     action_bits.set_action(ContextSyncSeqNum::try_from(15).unwrap());
     let action_de = ActionsDataElement::from(action_bits);
-    assert_eq!(15, action_de.context_sync_seq_num().as_u8());
+    assert_eq!(15, action_de.action.context_sync_seq_num().as_u8());
 }
 
 #[test]
 fn context_sync_seq_num_default_zero() {
     let action_bits = ActionBits::<Plaintext>::default();
     let action_de = ActionsDataElement::from(action_bits);
-    assert_eq!(0, action_de.context_sync_seq_num().as_u8());
+    assert_eq!(0, action_de.action.context_sync_seq_num().as_u8());
 }
 
 #[test]
@@ -310,9 +310,9 @@
     action_bits.set_action(ContextSyncSeqNum::try_from(15).unwrap());
     action_bits.set_action(NearbyShare::from(true));
     let action_de = ActionsDataElement::from(action_bits);
-    assert_eq!(action_de.has_action(&ActionType::NearbyShare), Some(true));
-    assert_eq!(action_de.has_action(&ActionType::ActiveUnlock), Some(false));
-    assert_eq!(action_de.has_action(&ActionType::PhoneHub), Some(false));
+    assert_eq!(action_de.action.has_action(&ActionType::NearbyShare), Some(true));
+    assert_eq!(action_de.action.has_action(&ActionType::ActiveUnlock), Some(false));
+    assert_eq!(action_de.action.has_action(&ActionType::PhoneHub), Some(false));
 }
 
 #[test]
@@ -322,10 +322,10 @@
     action_bits.set_action(NearbyShare::from(true));
     action_bits.set_action(ActiveUnlock::from(true));
     let action_de = ActionsDataElement::from(action_bits);
-    assert_eq!(action_de.has_action(&ActionType::NearbyShare), Some(true));
-    assert_eq!(action_de.has_action(&ActionType::ActiveUnlock), Some(true));
-    assert_eq!(action_de.has_action(&ActionType::PhoneHub), Some(false));
-    assert_eq!(action_de.has_action(&ActionType::ContextSyncSeqNum), None);
+    assert_eq!(action_de.action.has_action(&ActionType::NearbyShare), Some(true));
+    assert_eq!(action_de.action.has_action(&ActionType::ActiveUnlock), Some(true));
+    assert_eq!(action_de.action.has_action(&ActionType::PhoneHub), Some(false));
+    assert_eq!(action_de.action.has_action(&ActionType::ContextSyncSeqNum), None);
 }
 
 // hypothetical action using the last bit
diff --git a/nearby/presence/np_adv/src/legacy/data_elements.rs b/nearby/presence/np_adv/src/legacy/data_elements.rs
index e9c2308..505fdd9 100644
--- a/nearby/presence/np_adv/src/legacy/data_elements.rs
+++ b/nearby/presence/np_adv/src/legacy/data_elements.rs
@@ -58,7 +58,8 @@
 /// Data element holding a [TxPower].
 #[derive(Debug, PartialEq, Eq)]
 pub struct TxPowerDataElement {
-    tx_power: TxPower,
+    /// The tx power value
+    pub tx_power: TxPower,
 }
 
 impl TxPowerDataElement {
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/mod.rs b/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
index 3119f48..48555be 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/mod.rs
@@ -19,14 +19,15 @@
 extern crate alloc;
 
 use crate::{
+    credential::v0::V0,
     de_type::EncryptedIdentityDataElementType,
     legacy::{
         actions,
         data_elements::{DataElement, *},
         de_type::{DataElementType, DeEncodedLength, DeTypeCode, PlainDataElementType},
-        Ciphertext, PacketFlavor, Plaintext, NP_MAX_DE_CONTENT_LEN,
+        Ciphertext, PacketFlavor, Plaintext, ShortMetadataKey, NP_MAX_DE_CONTENT_LEN,
     },
-    PlaintextIdentityMode,
+    HasIdentityMatch, PlaintextIdentityMode,
 };
 use alloc::vec::Vec;
 use crypto_provider::CryptoProvider;
@@ -42,10 +43,15 @@
     input: &[u8],
 ) -> Result<IntermediateAdvContents, AdvDeserializeError> {
     parse_raw_adv_contents::<C>(input).and_then(|raw_adv| match raw_adv {
-        RawAdvertisement::Plaintext(parc) => parc
-            .try_deserialize()
-            .map(IntermediateAdvContents::Plaintext)
-            .map_err(AdvDeserializeError::DataElementDeserializeError),
+        RawAdvertisement::Plaintext(parc) => {
+            if parc.data_elements.is_empty() {
+                return Err(AdvDeserializeError::NoPublicDataElements);
+            }
+
+            parc.try_deserialize()
+                .map(IntermediateAdvContents::Plaintext)
+                .map_err(AdvDeserializeError::DataElementDeserializeError)
+        }
         RawAdvertisement::Ciphertext(eac) => Ok(IntermediateAdvContents::Ciphertext(eac)),
     })
 }
@@ -100,12 +106,7 @@
             }),
         }
     } else {
-        plain_data_elements(&data_elements).map(|pde| {
-            RawAdvertisement::Plaintext(PlaintextAdvRawContents {
-                identity_type: PlaintextIdentityMode::None,
-                data_elements: pde,
-            })
-        })
+        Err(AdvDeserializeError::MissingIdentity)
     }
 }
 
@@ -120,6 +121,10 @@
     TooManyTopLevelDataElements,
     /// Must not have an identity DE inside an identity DE
     InvalidDataElementHierarchy,
+    /// Missing identity DE
+    MissingIdentity,
+    /// Non-identity DE contents must not be empty
+    NoPublicDataElements,
 }
 
 /// Parse an advertisement's contents into raw DEs.
@@ -284,7 +289,6 @@
             .map_err(|_e| DecryptError::DecryptOrVerifyError)?;
 
         // plaintext starts with 14 bytes of metadata key, then DEs.
-
         let (remaining, metadata_key) = combinator::map_res(
             bytes::complete::take(NP_LEGACY_METADATA_KEY_LEN),
             |slice: &[u8]| slice.try_into(),
@@ -308,7 +312,7 @@
             .map(|data_elements| {
                 DecryptedAdvContents::new(
                     self.identity_type,
-                    metadata_key,
+                    ShortMetadataKey(metadata_key),
                     self.salt,
                     data_elements,
                 )
@@ -351,10 +355,12 @@
     pub fn identity(&self) -> PlaintextIdentityMode {
         self.identity_type
     }
-    /// Returns the deserialized data elements
-    pub fn data_elements(&self) -> impl Iterator<Item = &PlainDataElement<Plaintext>> {
+
+    /// Returns an iterator over the v0 data elements
+    pub fn data_elements(&self) -> impl ExactSizeIterator<Item = &PlainDataElement<Plaintext>> {
         self.data_elements.iter()
     }
+
     /// Destructures this V0 plaintext advertisement
     /// into just the contained data elements
     pub fn to_data_elements(self) -> Vec<PlainDataElement<Plaintext>> {
@@ -366,7 +372,7 @@
 #[derive(Debug, PartialEq, Eq)]
 pub struct DecryptedAdvContents {
     identity_type: EncryptedIdentityDataElementType,
-    metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN],
+    metadata_key: ShortMetadataKey,
     salt: LegacySalt,
     data_elements: Vec<PlainDataElement<Ciphertext>>,
 }
@@ -375,7 +381,7 @@
     /// Returns a new DecryptedAdvContents with the provided contents.
     fn new(
         identity_type: EncryptedIdentityDataElementType,
-        metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN],
+        metadata_key: ShortMetadataKey,
         salt: LegacySalt,
         data_elements: Vec<PlainDataElement<Ciphertext>>,
     ) -> Self {
@@ -387,15 +393,10 @@
         self.identity_type
     }
 
-    /// The decrypted metadata key from the identity DE.
-    pub fn metadata_key(&self) -> &[u8; 14] {
-        &self.metadata_key
-    }
-
     /// Iterator over the data elements in an advertisement, except for any DEs related to resolving
     /// the identity or otherwise validating the payload (e.g. any identity DEs like Private
     /// Identity).
-    pub fn data_elements(&self) -> impl Iterator<Item = &PlainDataElement<Ciphertext>> {
+    pub fn data_elements(&self) -> impl ExactSizeIterator<Item = &PlainDataElement<Ciphertext>> {
         self.data_elements.iter()
     }
 
@@ -405,6 +406,13 @@
     }
 }
 
+impl HasIdentityMatch for DecryptedAdvContents {
+    type Version = V0;
+    fn metadata_key(&self) -> ShortMetadataKey {
+        self.metadata_key
+    }
+}
+
 /// The contents of an advertisement after plaintext DEs, if any, have been deserialized, but
 /// before any decryption is done.
 #[derive(Debug, PartialEq, Eq)]
diff --git a/nearby/presence/np_adv/src/legacy/deserialize/tests.rs b/nearby/presence/np_adv/src/legacy/deserialize/tests.rs
index b577cd9..4512bf7 100644
--- a/nearby/presence/np_adv/src/legacy/deserialize/tests.rs
+++ b/nearby/presence/np_adv/src/legacy/deserialize/tests.rs
@@ -18,6 +18,7 @@
 use crate::legacy::actions::{ActionBits, ActionsDataElement};
 use crate::shared_data::TxPower;
 use crate::{
+    credential::{v0::V0, SimpleBroadcastCryptoMaterial},
     de_type::IdentityDataElementType,
     legacy::{
         actions,
@@ -29,7 +30,7 @@
         },
         PacketFlavorEnum, BLE_ADV_SVC_CONTENT_LEN,
     },
-    parse_adv_header, shared_data, AdvHeader, NoIdentity, PublicIdentity,
+    parse_adv_header, shared_data, AdvHeader, PublicIdentity,
 };
 use array_view::ArrayView;
 use crypto_provider_default::CryptoProviderImpl;
@@ -42,30 +43,8 @@
 
 #[test]
 fn parse_empty_raw_adv() {
-    let data_elements = parse_raw_adv_contents::<CryptoProviderImpl>(&[]).unwrap();
-    assert_eq!(
-        RawAdvertisement::Plaintext(PlaintextAdvRawContents {
-            identity_type: PlaintextIdentityMode::None,
-            data_elements: Vec::new()
-        }),
-        data_elements
-    );
-}
-
-#[test]
-fn parse_raw_adv_1_de_short_no_identity() {
-    // battery uses the header length as is
-    let adv = parse_raw_adv_contents::<CryptoProviderImpl>(&[0x36, 0x01, 0x02, 0x03]).unwrap();
-    assert_eq!(
-        RawAdvertisement::Plaintext(PlaintextAdvRawContents {
-            identity_type: PlaintextIdentityMode::None,
-            data_elements: vec![RawPlainDataElement {
-                de_type: PlainDataElementType::Actions,
-                contents: &[0x01, 0x02, 0x03]
-            }],
-        }),
-        adv
-    );
+    let adv_data = parse_raw_adv_contents::<CryptoProviderImpl>(&[]);
+    assert_eq!(AdvDeserializeError::MissingIdentity, adv_data.unwrap_err());
 }
 
 #[test]
@@ -161,7 +140,7 @@
         0x15, 0x03, // tx power de
     ];
     assert_eq!(
-        AdvDeserializeError::InvalidDataElementHierarchy,
+        AdvDeserializeError::MissingIdentity,
         parse_raw_adv_contents::<CryptoProviderImpl>(input).unwrap_err()
     );
 }
@@ -373,13 +352,19 @@
 }
 
 #[test]
-fn plaintext_random_adv_contents_round_trip_public() {
-    plaintext_random_adv_contents_round_trip(PublicIdentity::default, PlaintextIdentityMode::Public)
+fn deserialize_adv_public_identity_empty_des() {
+    let input = &[
+        0x03, // public identity
+    ];
+    assert_eq!(
+        AdvDeserializeError::NoPublicDataElements,
+        deserialize_adv_contents::<CryptoProviderImpl>(input).unwrap_err()
+    );
 }
 
 #[test]
-fn plaintext_random_adv_contents_round_trip_no_identity() {
-    plaintext_random_adv_contents_round_trip(NoIdentity::default, PlaintextIdentityMode::None)
+fn plaintext_random_adv_contents_round_trip_public() {
+    plaintext_random_adv_contents_round_trip(PublicIdentity::default, PlaintextIdentityMode::Public)
 }
 
 #[test]
@@ -399,15 +384,17 @@
         let salt: ldt_np_adv::LegacySalt = rng.gen::<[u8; 2]>().into();
         let metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN] = rng.gen();
         let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-        let ldt_key = hkdf.legacy_ldt_key();
         let metadata_key_hmac: [u8; 32] =
             hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&metadata_key);
         let cipher = ldt_np_adv::build_np_adv_decrypter_from_key_seed(&hkdf, metadata_key_hmac);
+
+        let metadata_key = ShortMetadataKey(metadata_key);
+        let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, metadata_key);
+
         let mut builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
             identity_type,
             salt,
-            metadata_key,
-            LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(&ldt_key),
+            &broadcast_cm,
         ));
 
         loop {
@@ -462,15 +449,18 @@
     let metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN] = [0x33; NP_LEGACY_METADATA_KEY_LEN];
 
     let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
-    let ldt_key = hkdf.legacy_ldt_key();
     let metadata_key_hmac: [u8; 32] =
         hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&metadata_key);
     let cipher = ldt_np_adv::build_np_adv_decrypter_from_key_seed(&hkdf, metadata_key_hmac);
+
+    let metadata_key = ShortMetadataKey(metadata_key);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, metadata_key);
+
     let mut builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
         EncryptedIdentityDataElementType::Private,
         salt,
-        metadata_key,
-        LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(&ldt_key),
+        &broadcast_cm,
     ));
 
     let tx = shared_data::TxPower::try_from(3).unwrap();
@@ -524,7 +514,7 @@
 
 #[test]
 fn decrypt_and_deserialize_plaintext_adv_canned() {
-    let mut builder = AdvBuilder::new(PublicIdentity::default());
+    let mut builder = AdvBuilder::new(PublicIdentity);
 
     let actions = ActionBits::default();
     builder.add_data_element(ActionsDataElement::from(actions)).unwrap();
@@ -694,18 +684,19 @@
     correct_key_seed: [u8; 32],
 ) -> (ArrayView<u8, { BLE_ADV_SVC_CONTENT_LEN }>, ldt_np_adv::LdtNpAdvDecrypterXtsAes128<C>) {
     let hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&correct_key_seed);
-    let ldt_key = hkdf.legacy_ldt_key();
 
     let metadata_key_hmac: [u8; 32] =
         hkdf.legacy_metadata_key_hmac_key().calculate_hmac(metadata_key.as_slice());
 
     let correct_cipher = ldt_np_adv::build_np_adv_decrypter_from_key_seed(&hkdf, metadata_key_hmac);
 
+    let broadcast_cm =
+        SimpleBroadcastCryptoMaterial::<V0>::new(correct_key_seed, ShortMetadataKey(*metadata_key));
+
     let mut builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
         EncryptedIdentityDataElementType::Private,
         salt,
-        *metadata_key,
-        LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(&ldt_key),
+        &broadcast_cm,
     ));
     builder.add_data_element(TxPowerDataElement::from(TxPower::try_from(3).unwrap())).unwrap();
     (builder.into_advertisement().unwrap(), correct_cipher)
diff --git a/nearby/presence/np_adv/src/legacy/mod.rs b/nearby/presence/np_adv/src/legacy/mod.rs
index 0eb035d..59f2308 100644
--- a/nearby/presence/np_adv/src/legacy/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/mod.rs
@@ -14,7 +14,10 @@
 
 //! V0 advertisement support.
 
+use crate::MetadataKey;
 use core::fmt;
+use crypto_provider::CryptoProvider;
+use ldt_np_adv::NP_LEGACY_METADATA_KEY_LEN;
 
 pub mod actions;
 pub mod data_elements;
@@ -31,6 +34,26 @@
 /// Maximum possible DE content: packet size minus 2 for adv header & DE header
 const NP_MAX_DE_CONTENT_LEN: usize = BLE_ADV_SVC_CONTENT_LEN - 2;
 
+/// "Short" 14-byte metadata key type employed for V0, which needs to be
+/// expanded to a regular-size 16-byte metadata key to decrypt metadata.
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub struct ShortMetadataKey(pub [u8; NP_LEGACY_METADATA_KEY_LEN]);
+
+impl AsRef<[u8]> for ShortMetadataKey {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl ShortMetadataKey {
+    /// Expand this short 14-byte metadata key to a 16-byte metadata key
+    /// which may be used to decrypt metadata.
+    pub fn expand<C: CryptoProvider>(&self) -> MetadataKey {
+        let expanded_bytes = np_hkdf::legacy_metadata_expanded_key::<C>(&self.0);
+        MetadataKey(expanded_bytes)
+    }
+}
+
 /// Marker type to allow disambiguating between plaintext and encrypted packets at compile time.
 ///
 /// See also [PacketFlavorEnum] for when runtime flavor checks are more suitable.
diff --git a/nearby/presence/np_adv/src/legacy/serialize/mod.rs b/nearby/presence/np_adv/src/legacy/serialize/mod.rs
index 38e46e1..2920b67 100644
--- a/nearby/presence/np_adv/src/legacy/serialize/mod.rs
+++ b/nearby/presence/np_adv/src/legacy/serialize/mod.rs
@@ -40,24 +40,29 @@
 //! Serializing an encrypted advertisement:
 //!
 //! ```
-//! use np_adv::{shared_data::*, de_type::*, legacy::{de_type::*, data_elements::*, serialize::*}};
+//! use np_adv::{shared_data::*, de_type::*, legacy::{de_type::*, data_elements::*, serialize::*, *}};
+//! use np_adv::credential::{v0::V0, SimpleBroadcastCryptoMaterial};
 //! use crypto_provider::CryptoProvider;
 //! use crypto_provider_default::CryptoProviderImpl;
 //! use ldt_np_adv::{salt_padder, LegacySalt, LdtEncrypterXtsAes128};
 //!
 //! // Generate these from proper CSPRNGs -- using fixed data here
-//! let metadata_key = [0x33; 14];
+//! let metadata_key = ShortMetadataKey([0x33; 14]);
 //! let salt = LegacySalt::from([0x01, 0x02]);
 //! let key_seed = [0x44; 32];
 //! let ldt_enc = LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(
 //!     &np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed).legacy_ldt_key()
 //! );
 //!
+//! let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(
+//!     key_seed,
+//!     metadata_key,
+//! );
+//!
 //! let mut builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
 //!     EncryptedIdentityDataElementType::Private,
 //!     salt,
-//!     metadata_key,
-//!     ldt_enc,
+//!     &broadcast_cm,
 //! ));
 //!
 //! builder
@@ -66,18 +71,19 @@
 //!
 //! let packet = builder.into_advertisement().unwrap();
 //! ```
+use crate::credential::{v0::V0, BroadcastCryptoMaterial};
 use crate::{
     de_type::{EncryptedIdentityDataElementType, IdentityDataElementType},
     legacy::{
         de_type::{DataElementType, DeActualLength, DeEncodedLength, PlainDataElementType},
-        Ciphertext, PacketFlavor, Plaintext, BLE_ADV_SVC_CONTENT_LEN, NP_MAX_DE_CONTENT_LEN,
+        Ciphertext, PacketFlavor, Plaintext, ShortMetadataKey, BLE_ADV_SVC_CONTENT_LEN,
+        NP_MAX_DE_CONTENT_LEN,
     },
-    DeLengthOutOfRange, NoIdentity, PublicIdentity,
+    DeLengthOutOfRange, PublicIdentity,
 };
 use array_view::ArrayView;
 use core::{convert, fmt, marker};
 use crypto_provider::CryptoProvider;
-use ldt_np_adv::NP_LEGACY_METADATA_KEY_LEN;
 
 #[cfg(test)]
 mod tests;
@@ -121,26 +127,13 @@
     }
 }
 
-impl Identity for NoIdentity {
-    type Flavor = Plaintext;
-    type Error = convert::Infallible;
-    // No DE header
-    const OVERHEAD_LEN: usize = 0;
-
-    fn postprocess(&self, _buf: &mut [u8]) -> Result<(), Self::Error> {
-        Ok(())
-    }
-}
-
 /// Identity used for encrypted packets (private, trusted, provisioned) that encrypts other DEs
 /// (as well as the metadata key).
 pub struct LdtIdentity<C: CryptoProvider> {
     de_type: EncryptedIdentityDataElementType,
     salt: ldt_np_adv::LegacySalt,
-    metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN],
+    metadata_key: ShortMetadataKey,
     ldt_enc: ldt_np_adv::LdtEncrypterXtsAes128<C>,
-    // keep C parameter alive for when A disappears into it
-    _crypto_provider: marker::PhantomData<C>,
 }
 
 // Exclude sensitive members
@@ -151,14 +144,20 @@
 }
 
 impl<C: CryptoProvider> LdtIdentity<C> {
-    /// Build an `LdtIdentity` for the provided identity, salt, metadata key, and ldt.
-    pub fn new(
+    /// Build an `LdtIdentity` for the provided identity type, salt, and
+    /// broadcast crypto-materials.
+    pub fn new<B: BroadcastCryptoMaterial<V0>>(
         de_type: EncryptedIdentityDataElementType,
         salt: ldt_np_adv::LegacySalt,
-        metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN],
-        ldt_enc: ldt_np_adv::LdtEncrypterXtsAes128<C>,
-    ) -> LdtIdentity<C> {
-        LdtIdentity { de_type, salt, metadata_key, ldt_enc, _crypto_provider: marker::PhantomData }
+        crypto_material: &B,
+    ) -> Self {
+        let metadata_key = crypto_material.metadata_key();
+        let key_seed = crypto_material.key_seed();
+        let key_seed_hkdf = np_hkdf::NpKeySeedHkdf::<C>::new(&key_seed);
+        let ldt_key = key_seed_hkdf.legacy_ldt_key();
+        let ldt_enc = ldt_np_adv::LdtEncrypterXtsAes128::<C>::new(&ldt_key);
+
+        Self { de_type, salt, metadata_key, ldt_enc }
     }
 }
 
@@ -180,7 +179,7 @@
         buf[0] = encode_de_header_actual_len(id_de_type_as_generic_de_type(de_type), actual_len)
             .map_err(|_e| LdtPostprocessError::InvalidLength)?;
         buf[1..3].copy_from_slice(self.salt.bytes().as_slice());
-        buf[3..17].copy_from_slice(&self.metadata_key);
+        buf[3..17].copy_from_slice(&self.metadata_key.0);
 
         // encrypt everything after DE header and salt
         self.ldt_enc.encrypt(&mut buf[3..], &ldt_np_adv::salt_padder::<16, C>(self.salt)).map_err(
diff --git a/nearby/presence/np_adv/src/legacy/serialize/tests.rs b/nearby/presence/np_adv/src/legacy/serialize/tests.rs
index 192b8f4..b4adc9f 100644
--- a/nearby/presence/np_adv/src/legacy/serialize/tests.rs
+++ b/nearby/presence/np_adv/src/legacy/serialize/tests.rs
@@ -17,6 +17,7 @@
 use crate::legacy::actions::FastPairSass;
 use crate::legacy::actions::NearbyShare;
 use crate::{
+    credential::{v0::V0, SimpleBroadcastCryptoMaterial},
     de_type::EncryptedIdentityDataElementType,
     legacy::{actions::*, data_elements::*, serialize::*},
     shared_data::TxPower,
@@ -27,7 +28,7 @@
 
 #[test]
 fn public_identity_packet_serialization() {
-    let mut builder = AdvBuilder::new(PublicIdentity::default());
+    let mut builder = AdvBuilder::new(PublicIdentity);
 
     let tx_power = TxPower::try_from(3).unwrap();
     let mut action = ActionBits::default();
@@ -48,29 +49,8 @@
 }
 
 #[test]
-fn no_identity_packet_serialization() {
-    let mut builder = AdvBuilder::new(NoIdentity::default());
-
-    let tx_power = TxPower::try_from(3).unwrap();
-    let mut action = ActionBits::default();
-    action.set_action(NearbyShare::from(true));
-    builder.add_data_element(TxPowerDataElement::from(tx_power)).unwrap();
-    builder.add_data_element(ActionsDataElement::from(action)).unwrap();
-
-    let packet = builder.into_advertisement().unwrap();
-    assert_eq!(
-        &[
-            0x00, // Adv Header
-            0x15, 0x03, // Tx Power DE with value 3
-            0x26, 0x00, 0x40, // Actions DE w/ bit 9
-        ],
-        packet.as_slice()
-    );
-}
-
-#[test]
 fn packet_limits_capacity() {
-    let mut builder = AdvBuilder::new(PublicIdentity::default());
+    let mut builder = AdvBuilder::new(PublicIdentity);
     // 2 + 1 left out of 24 payload bytes
     builder.len = 21;
     let mut bits = ActionBits::default();
@@ -90,22 +70,24 @@
 #[test]
 fn ldt_packet_serialization() {
     // don't care about the HMAC since we're not decrypting
-    let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&[0; 32]);
+    let key_seed = [0; 32];
+    let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
     let ldt = LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(&hkdf.legacy_ldt_key());
-    let metadata_key = [0x33; 14];
+    let metadata_key = ShortMetadataKey([0x33; 14]);
     let salt = LegacySalt::from([0x01, 0x02]);
 
     let mut ciphertext = vec![];
-    ciphertext.extend_from_slice(&metadata_key);
+    ciphertext.extend_from_slice(&metadata_key.0);
     // tx power & action DEs
     ciphertext.extend_from_slice(&[0x15, 0x03, 0x26, 0x00, 0x10]);
     ldt.encrypt(&mut ciphertext, &salt_padder::<16, CryptoProviderImpl>(salt)).unwrap();
 
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, metadata_key);
+
     let mut builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
         EncryptedIdentityDataElementType::Private,
         salt,
-        metadata_key,
-        ldt,
+        &broadcast_cm,
     ));
 
     let tx_power = TxPower::try_from(3).unwrap();
@@ -126,16 +108,16 @@
 
 #[test]
 fn ldt_packet_cant_encrypt_without_des() {
-    let metadata_key = [0x33; 14];
+    let metadata_key = ShortMetadataKey([0x33; 14]);
     let salt = LegacySalt::from([0x01, 0x02]);
+    let key_seed = [0xFE; 32];
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, metadata_key);
 
     let builder = AdvBuilder::new(LdtIdentity::<CryptoProviderImpl>::new(
         EncryptedIdentityDataElementType::Private,
         salt,
-        metadata_key,
-        LdtEncrypterXtsAes128::<CryptoProviderImpl>::new(
-            &np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&[0xFE; 32]).legacy_ldt_key(),
-        ),
+        &broadcast_cm,
     ));
 
     // not enough ciphertext
@@ -144,7 +126,7 @@
 
 #[test]
 fn nearby_share_action() {
-    let mut builder = AdvBuilder::new(PublicIdentity::default());
+    let mut builder = AdvBuilder::new(PublicIdentity);
 
     let mut action = ActionBits::default();
     action.set_action(NearbyShare::from(true));
diff --git a/nearby/presence/np_adv/src/lib.rs b/nearby/presence/np_adv/src/lib.rs
index a6a155e..45207ff 100644
--- a/nearby/presence/np_adv/src/lib.rs
+++ b/nearby/presence/np_adv/src/lib.rs
@@ -25,38 +25,48 @@
 extern crate core;
 use crate::{
     credential::{
-        source::{BothCredentialSource, CredentialSource},
-        v0::V0CryptoMaterial,
-        MatchedCredFromCred, MatchedCredential, V0Credential, V1Credential,
+        book::CredentialBook, v0::V0DiscoveryCryptoMaterial, v1::V1DiscoveryCryptoMaterial,
+        DiscoveryCryptoMaterial, MatchedCredential, ProtocolVersion,
     },
+    deserialization_arena::ArenaOutOfSpace,
     extended::deserialize::{
-        parse_sections, CiphertextSection, DataElements, DecryptedSection, IntermediateSection,
-        PlaintextSection, Section, SectionDeserializeError,
+        encrypted_section::*, parse_sections, CiphertextSection, DataElementParseError,
+        DataElementParsingIterator, DecryptedSection, IntermediateSection, PlaintextSection,
+        Section, SectionDeserializeError,
     },
     legacy::deserialize::{
         DecryptError, DecryptedAdvContents, IntermediateAdvContents, PlaintextAdvContents,
     },
 };
+
 use alloc::vec::Vec;
 #[cfg(feature = "devtools")]
 use array_view::ArrayView;
-use core::{fmt::Debug, marker};
+use core::borrow::BorrowMut;
+use core::fmt::Debug;
 use crypto_provider::CryptoProvider;
+use deserialization_arena::DeserializationArena;
+
 #[cfg(feature = "devtools")]
 use extended::NP_ADV_MAX_SECTION_LEN;
 use legacy::{data_elements::DataElementDeserializeError, deserialize::AdvDeserializeError};
 use nom::{combinator, number};
+pub use strum;
+
 pub mod credential;
 pub mod de_type;
 #[cfg(test)]
 mod deser_v0_tests;
 #[cfg(test)]
 mod deser_v1_tests;
+pub mod deserialization_arena;
 pub mod extended;
+pub mod filter;
 #[cfg(test)]
 mod header_parse_tests;
 pub mod legacy;
 pub mod shared_data;
+
 /// Canonical form of NP's service UUID.
 ///
 /// Note that UUIDs are encoded in BT frames in little-endian order, so these bytes may need to be
@@ -65,73 +75,40 @@
 
 /// Parse, deserialize, decrypt, and validate a complete NP advertisement (the entire contents of
 /// the service data for the NP UUID).
-pub fn deserialize_advertisement<'s, C0, C1, M, S, P>(
-    adv: &[u8],
-    cred_source: &'s S,
-) -> Result<DeserializedAdvertisement<'s, M>, AdvDeserializationError>
+pub fn deserialize_advertisement<'adv, 'cred, B, P>(
+    mut arena: impl BorrowMut<DeserializationArena<'adv>>,
+    adv: &'adv [u8],
+    cred_book: &'cred B,
+) -> Result<DeserializedAdvertisement<'adv, B::Matched>, AdvDeserializationError>
 where
-    C0: V0Credential<Matched<'s> = M> + 's,
-    C1: V1Credential<Matched<'s> = M> + 's,
-    M: MatchedCredential<'s>,
-    S: BothCredentialSource<C0, C1>,
+    B: CredentialBook<'cred>,
+    P: CryptoProvider,
+{
+    // Turn `impl BorrowMut` into the concrete `&mut` reference so that the compiler does not need
+    // to duplicate the function code for different concrete types.
+    deserialize_advertisement_impl::<B, P>(arena.borrow_mut(), adv, cred_book)
+}
+
+fn deserialize_advertisement_impl<'adv, 'cred, B, P>(
+    arena: &mut DeserializationArena<'adv>,
+    adv: &'adv [u8],
+    cred_book: &'cred B,
+) -> Result<DeserializedAdvertisement<'adv, B::Matched>, AdvDeserializationError>
+where
+    B: CredentialBook<'cred>,
     P: CryptoProvider,
 {
     let (remaining, header) =
         parse_adv_header(adv).map_err(|_e| AdvDeserializationError::HeaderParseError)?;
     match header {
-        AdvHeader::V1(header) => {
-            deser_decrypt_v1::<C1, S::V1Source, P>(cred_source.v1(), remaining, header)
-                .map(DeserializedAdvertisement::V1)
+        AdvHeader::V0 => {
+            deser_decrypt_v0::<B, P>(cred_book, remaining).map(DeserializedAdvertisement::V0)
         }
-        AdvHeader::V0 => deser_decrypt_v0::<C0, S::V0Source, P>(cred_source.v0(), remaining)
-            .map(DeserializedAdvertisement::V0),
+        AdvHeader::V1(header) => deser_decrypt_v1::<B, P>(arena, cred_book, remaining, header)
+            .map(DeserializedAdvertisement::V1),
     }
 }
 
-/// Parse, deserialize, decrypt, and validate a complete V0 NP advertisement (the entire contents
-/// of the service data for the NP UUID). If the advertisement version header does not match V0,
-/// this method will return an [`AdvDeserializationError::HeaderParseError`]
-pub fn deserialize_v0_advertisement<'s, C, S, P>(
-    adv: &[u8],
-    cred_source: &'s S,
-) -> Result<V0AdvertisementContents<'s, C>, AdvDeserializationError>
-where
-    C: V0Credential,
-    S: CredentialSource<C>,
-    P: CryptoProvider,
-{
-    let (remaining, header) =
-        parse_adv_header(adv).map_err(|_e| AdvDeserializationError::HeaderParseError)?;
-
-    match header {
-        AdvHeader::V0 => deser_decrypt_v0::<C, S, P>(cred_source, remaining),
-        AdvHeader::V1(_) => Err(AdvDeserializationError::HeaderParseError),
-    }
-}
-
-/// Parse, deserialize, decrypt, and validate a complete V1 NP advertisement (the entire contents
-/// of the service data for the NP UUID). If the advertisement version header does not match V1,
-/// this method will return an [`AdvDeserializationError::HeaderParseError`]
-pub fn deserialize_v1_advertisement<'s, C, S, P>(
-    adv: &[u8],
-    cred_source: &'s S,
-) -> Result<V1AdvertisementContents<'s, C>, AdvDeserializationError>
-where
-    C: V1Credential,
-    S: CredentialSource<C>,
-    P: CryptoProvider,
-{
-    let (remaining, header) =
-        parse_adv_header(adv).map_err(|_e| AdvDeserializationError::HeaderParseError)?;
-
-    match header {
-        AdvHeader::V0 => Err(AdvDeserializationError::HeaderParseError),
-        AdvHeader::V1(header) => deser_decrypt_v1::<C, S, P>(cred_source, remaining, header),
-    }
-}
-
-type V1AdvertisementContents<'s, C> = V1AdvContents<'s, MatchedCredFromCred<'s, C>>;
-
 /// The encryption scheme used for a V1 advertisement.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum V1EncryptionScheme {
@@ -151,157 +128,282 @@
     ParseError,
     /// No suitable credential found to decrypt the given section.
     NoMatchingCredentials,
+    /// Given Arena is not large enough to hold the decrypted data.
+    ArenaOutOfSpace,
+}
+
+#[cfg(feature = "devtools")]
+impl From<ArenaOutOfSpace> for AdvDecryptionError {
+    fn from(_: ArenaOutOfSpace) -> Self {
+        Self::ArenaOutOfSpace
+    }
 }
 
 /// Decrypt, but do not further deserialize the v1 bytes, intended for developer tooling uses only.
-/// Production uses should use [deserialize_v1_advertisement] instead, which deserializes to a
+/// Production uses should use [deserialize_advertisement] instead, which deserializes to a
 /// structured format and provides extra type safety.
 #[cfg(feature = "devtools")]
-pub fn deser_decrypt_v1_section_bytes_for_dev_tools<S, V1, P>(
-    cred_source: &S,
+pub fn deser_decrypt_v1_section_bytes_for_dev_tools<'adv, 'cred, B, P>(
+    arena: &mut DeserializationArena<'adv>,
+    cred_book: &'cred B,
     header_byte: u8,
-    section_bytes: &[u8],
+    section_bytes: &'adv [u8],
 ) -> Result<(ArrayView<u8, NP_ADV_MAX_SECTION_LEN>, V1EncryptionScheme), AdvDecryptionError>
 where
-    S: CredentialSource<V1>,
-    V1: V1Credential,
+    B: CredentialBook<'cred>,
     P: CryptoProvider,
 {
     let header = V1Header { header_byte };
     let int_sections =
-        parse_sections(&header, section_bytes).map_err(|_| AdvDecryptionError::ParseError)?;
+        parse_sections(header, section_bytes).map_err(|_| AdvDecryptionError::ParseError)?;
     let cipher_section = match &int_sections[0] {
         IntermediateSection::Plaintext(_) => Err(AdvDecryptionError::InputNotEncrypted)?,
         IntermediateSection::Ciphertext(section) => section,
     };
 
-    use crate::credential::v1::V1CryptoMaterial;
-    use core::borrow::Borrow;
-
-    for cred in cred_source.iter() {
-        let crypto_material = cred.crypto_material();
-
-        match cipher_section {
-            CiphertextSection::SignatureEncryptedIdentity(encrypted_section) => {
-                let identity_resolution_material =
-                    crypto_material.signed_identity_resolution_material::<P>();
-                match encrypted_section.try_decrypt::<P>(identity_resolution_material.borrow()) {
-                    Ok(plaintext) => return Ok((plaintext, V1EncryptionScheme::Signature)),
-                    Err(_) => continue,
-                }
-            }
-            CiphertextSection::MicEncryptedIdentity(encrypted_section) => {
-                let identity_resolution_material =
-                    crypto_material.unsigned_identity_resolution_material::<P>();
-                let verification_material = crypto_material.unsigned_verification_material::<P>();
-                match encrypted_section
-                    .try_decrypt::<P>(identity_resolution_material.borrow(), &verification_material)
-                {
-                    Ok(plaintext) => return Ok((plaintext, V1EncryptionScheme::Mic)),
-                    Err(_) => continue,
-                }
-            }
+    for (crypto_material, _) in cred_book.v1_iter() {
+        if let Some(plaintext) = cipher_section
+            .try_resolve_identity_and_decrypt::<_, P>(arena.borrow_mut(), &crypto_material)
+        {
+            return plaintext
+                .map(|pt| {
+                    let encryption_scheme = match cipher_section {
+                        CiphertextSection::SignatureEncryptedIdentity(_) => {
+                            V1EncryptionScheme::Signature
+                        }
+                        CiphertextSection::MicEncryptedIdentity(_) => V1EncryptionScheme::Mic,
+                    };
+                    (pt, encryption_scheme)
+                })
+                .map_err(|e| e.into());
         }
     }
     Err(AdvDecryptionError::NoMatchingCredentials)
 }
 
-/// Deserialize and decrypt the contents of a v1 adv after the version header
-fn deser_decrypt_v1<'s, C, S, P>(
-    cred_source: &'s S,
-    remaining: &[u8],
-    header: V1Header,
-) -> Result<V1AdvertisementContents<'s, C>, AdvDeserializationError>
-where
-    C: V1Credential,
-    S: CredentialSource<C>,
-    P: CryptoProvider,
-{
-    let int_sections =
-        parse_sections(&header, remaining).map_err(|_| AdvDeserializationError::ParseError {
-            details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError,
-        })?;
-    let mut sections = Vec::new();
-    let mut to_decrypt = Vec::new();
-    // keep track of ordering for later sorting
-    for (idx, s) in int_sections.into_iter().enumerate() {
-        match s {
-            IntermediateSection::Plaintext(p) => {
-                sections.push((idx, V1DeserializedSection::Plaintext(p)))
+/// A ciphertext section which has not yet been
+/// resolved to an identity, but for which some
+/// `SectionIdentityResolutionContents` have been
+/// pre-computed for speedy identity-resolution.
+struct ResolvableCiphertextSection<'a> {
+    identity_resolution_contents: SectionIdentityResolutionContents,
+    ciphertext_section: CiphertextSection<'a>,
+}
+
+/// A collection of possibly-deserialized sections which are separated according
+/// to whether/not they're intermediate encrypted sections (of either type)
+/// or fully-deserialized, with a running count of the number of malformed sections.
+/// Each potentially-valid section is tagged with a 0-based index derived from the original
+/// section ordering as they appeared within the original advertisement to ensure
+/// that the fully-deserialized advertisement may be correctly reconstructed.
+struct SectionsInProcessing<'adv, M: MatchedCredential> {
+    deserialized_sections: Vec<(usize, V1DeserializedSection<'adv, M>)>,
+    encrypted_sections: Vec<(usize, ResolvableCiphertextSection<'adv>)>,
+    malformed_sections_count: usize,
+}
+
+impl<'adv, M: MatchedCredential> SectionsInProcessing<'adv, M> {
+    /// Attempts to parse a V1 advertisement's contents after the version header
+    /// into a collection of not-yet-fully-deserialized sections which may
+    /// require credentials to be decrypted.
+    fn from_advertisement_contents<C: CryptoProvider>(
+        header: V1Header,
+        remaining: &'adv [u8],
+    ) -> Result<Self, AdvDeserializationError> {
+        let int_sections =
+            parse_sections(header, remaining).map_err(|_| AdvDeserializationError::ParseError {
+                details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError,
+            })?;
+        let mut deserialized_sections = Vec::new();
+        let mut encrypted_sections = Vec::new();
+        // keep track of ordering for later sorting during `self.finished_with_decryption_attempts()`.
+        for (idx, s) in int_sections.into_iter().enumerate() {
+            match s {
+                IntermediateSection::Plaintext(p) => {
+                    deserialized_sections.push((idx, V1DeserializedSection::Plaintext(p)))
+                }
+                IntermediateSection::Ciphertext(ciphertext_section) => {
+                    let identity_resolution_contents =
+                        ciphertext_section.contents().compute_identity_resolution_contents::<C>();
+                    let resolvable_ciphertext_section = ResolvableCiphertextSection {
+                        identity_resolution_contents,
+                        ciphertext_section,
+                    };
+                    encrypted_sections.push((idx, resolvable_ciphertext_section));
+                }
             }
-            IntermediateSection::Ciphertext(c) => to_decrypt.push((idx, c)),
         }
+        Ok(Self { deserialized_sections, encrypted_sections, malformed_sections_count: 0 })
     }
-    let mut invalid_sections = 0;
-    // Hot loop
-    // We assume that iterating credentials is more expensive than iterating sections
-    for cred in cred_source.iter() {
+
+    /// Returns true iff we have resolved all sections to identities.
+    fn resolved_all_identities(&self) -> bool {
+        self.encrypted_sections.is_empty()
+    }
+
+    /// Runs through all of the encrypted sections in processing, and attempts
+    /// to use the given credential to decrypt them. Suitable for situations
+    /// where iterating over credentials is relatively slow compared to
+    /// the cost of iterating over sections-in-memory.
+    fn try_decrypt_with_credential<C: V1DiscoveryCryptoMaterial, P: CryptoProvider>(
+        &mut self,
+        arena: &mut DeserializationArena<'adv>,
+        crypto_material: C,
+        match_data: M,
+    ) -> Result<(), ArenaOutOfSpace> {
         let mut i = 0;
-        while i < to_decrypt.len() {
-            let (section_idx, c): &(usize, CiphertextSection) = &to_decrypt[i];
-            match c.try_deserialize::<C, P>(cred) {
-                Ok(s) => {
-                    sections.push((
-                        *section_idx,
-                        V1DeserializedSection::Decrypted(WithMatchedCredential::new(
-                            cred.matched(),
-                            s,
-                        )),
-                    ));
-                    // we don't care about maintaining order, so use O(1) remove
-                    to_decrypt.swap_remove(i);
+        while i < self.encrypted_sections.len() {
+            let (section_idx, section): &(usize, ResolvableCiphertextSection) =
+                &self.encrypted_sections[i];
+            // Fast-path: Check for an identity match, ignore if there's no identity match.
+            let identity_resolution_contents = &section.identity_resolution_contents;
+            let identity_resolution_material = match &section.ciphertext_section {
+                CiphertextSection::MicEncryptedIdentity(_) => crypto_material
+                    .unsigned_identity_resolution_material::<P>()
+                    .into_raw_resolution_material(),
+                CiphertextSection::SignatureEncryptedIdentity(_) => crypto_material
+                    .signed_identity_resolution_material::<P>()
+                    .into_raw_resolution_material(),
+            };
+            match identity_resolution_contents.try_match::<P>(&identity_resolution_material) {
+                None => {
+                    // Try again with another section
+                    i += 1;
+                    continue;
+                }
+                Some(identity_match) => {
+                    // The identity matched, so now we need to more closely scrutinize
+                    // the provided ciphertext. Try to decrypt and parse the section.
+                    let metadata_nonce = crypto_material.metadata_nonce::<P>();
+                    let deserialization_result = match &section.ciphertext_section {
+                        CiphertextSection::SignatureEncryptedIdentity(c) => c
+                            .try_deserialize(
+                                arena.borrow_mut(),
+                                identity_match,
+                                &crypto_material.signed_verification_material::<P>(),
+                            )
+                            .map_err(SectionDeserializeError::from),
+                        CiphertextSection::MicEncryptedIdentity(c) => c
+                            .try_deserialize(
+                                arena.borrow_mut(),
+                                identity_match,
+                                &crypto_material.unsigned_verification_material::<P>(),
+                            )
+                            .map_err(SectionDeserializeError::from),
+                    };
+                    match deserialization_result {
+                        Ok(s) => {
+                            self.deserialized_sections.push((
+                                *section_idx,
+                                V1DeserializedSection::Decrypted(WithMatchedCredential::new(
+                                    match_data.clone(),
+                                    metadata_nonce,
+                                    s,
+                                )),
+                            ));
+                        }
+                        Err(e) => match e {
+                            SectionDeserializeError::IncorrectCredential => {
+                                // keep it around to try with another credential
+                                i += 1;
+                                continue;
+                            }
+                            SectionDeserializeError::ParseError => {
+                                // the credential worked, but the section itself was bogus
+                                self.malformed_sections_count += 1;
+                            }
+                            SectionDeserializeError::ArenaOutOfSpace => {
+                                return Err(ArenaOutOfSpace)
+                            }
+                        },
+                    }
+                    // By default, if we have an identity match, assume that decrypting the section worked,
+                    // or that the section was somehow invalid.
+                    // We don't care about maintaining order, so use O(1) remove
+                    self.encrypted_sections.swap_remove(i);
                     // don't advance i -- it now points to a new element
                 }
-                Err(e) => match e {
-                    SectionDeserializeError::IncorrectCredential => {
-                        // keep it around to try with another credential
-                        i += 1;
-                    }
-                    SectionDeserializeError::ParseError => {
-                        // the credential worked, but the section itself was bogus, so drop
-                        // it
-                        invalid_sections += 1;
-                        to_decrypt.swap_remove(i);
-                    }
-                },
             }
         }
-        if to_decrypt.is_empty() {
-            // no need to consider the remaining credentials
+        Ok(())
+    }
+
+    /// Packages the current state of the deserialization process into a
+    /// `V1AdvertisementContents` representing a fully-deserialized V1 advertisement.
+    ///
+    /// This method should only be called after all sections were either successfully
+    /// decrypted or have had all relevant credentials checked against
+    /// them without obtaining a successful identity-match and/or subsequent
+    /// cryptographic verification of the section contents.
+    fn finished_with_decryption_attempts(mut self) -> V1AdvertisementContents<'adv, M> {
+        // Invalid sections = malformed sections + number of encrypted sections
+        // which we could not manage to decrypt with any of our credentials
+        let invalid_sections_count = self.malformed_sections_count + self.encrypted_sections.len();
+
+        // Put the deserialized sections back into the original ordering for
+        // the returned `V1AdvertisementContents`
+        // (Note: idx is unique, so unstable sort is ok)
+        self.deserialized_sections.sort_unstable_by_key(|(idx, _section)| *idx);
+        let ordered_sections = self.deserialized_sections.into_iter().map(|(_idx, s)| s).collect();
+        V1AdvertisementContents::new(ordered_sections, invalid_sections_count)
+    }
+}
+
+/// Deserialize and decrypt the contents of a v1 adv after the version header
+fn deser_decrypt_v1<'adv, 'cred, B, P>(
+    arena: &mut DeserializationArena<'adv>,
+    cred_book: &'cred B,
+    remaining: &'adv [u8],
+    header: V1Header,
+) -> Result<V1AdvertisementContents<'adv, B::Matched>, AdvDeserializationError>
+where
+    B: CredentialBook<'cred>,
+    P: CryptoProvider,
+{
+    let mut sections_in_processing =
+        SectionsInProcessing::<'_, B::Matched>::from_advertisement_contents::<P>(
+            header, remaining,
+        )?;
+
+    // Hot loop
+    // We assume that iterating credentials is more expensive than iterating sections
+    for (crypto_material, match_data) in cred_book.v1_iter() {
+        sections_in_processing.try_decrypt_with_credential::<_, P>(
+            arena.borrow_mut(),
+            crypto_material,
+            match_data,
+        )?;
+        if sections_in_processing.resolved_all_identities() {
+            // No need to consider the other credentials
             break;
         }
     }
-    invalid_sections += to_decrypt.len();
-    // decryption may produce sections out of order
-    sections.sort_by_key(|(idx, _section)| *idx);
-    Ok(V1AdvContents::new(sections.into_iter().map(|(_idx, s)| s).collect(), invalid_sections))
+    Ok(sections_in_processing.finished_with_decryption_attempts())
 }
 
-type V0AdvertisementContents<'s, C> = V0AdvContents<'s, MatchedCredFromCred<'s, C>>;
-
 /// Deserialize and decrypt the contents of a v0 adv after the version header
-fn deser_decrypt_v0<'s, C, S, P>(
-    cred_source: &'s S,
+fn deser_decrypt_v0<'a, B, P>(
+    cred_book: &'a B,
     remaining: &[u8],
-) -> Result<V0AdvertisementContents<'s, C>, AdvDeserializationError>
+) -> Result<V0AdvertisementContents<B::Matched>, AdvDeserializationError>
 where
-    C: V0Credential,
-    S: CredentialSource<C>,
+    B: CredentialBook<'a>,
     P: CryptoProvider,
 {
     let contents = legacy::deserialize::deserialize_adv_contents::<P>(remaining)?;
-    return match contents {
-        IntermediateAdvContents::Plaintext(p) => Ok(V0AdvContents::Plaintext(p)),
+    match contents {
+        IntermediateAdvContents::Plaintext(p) => Ok(V0AdvertisementContents::Plaintext(p)),
         IntermediateAdvContents::Ciphertext(c) => {
-            for cred in cred_source.iter() {
-                let cm = cred.crypto_material();
-                let ldt = cm.ldt_adv_cipher::<P>();
+            for (crypto_material, matched) in cred_book.v0_iter() {
+                let ldt = crypto_material.ldt_adv_cipher::<P>();
                 match c.try_decrypt(&ldt) {
                     Ok(c) => {
-                        return Ok(V0AdvContents::Decrypted(WithMatchedCredential::new(
-                            cred.matched(),
+                        let metadata_nonce = crypto_material.metadata_nonce::<P>();
+                        return Ok(V0AdvertisementContents::Decrypted(WithMatchedCredential::new(
+                            matched,
+                            metadata_nonce,
                             c,
-                        )))
+                        )));
                     }
                     Err(e) => match e {
                         DecryptError::DecryptOrVerifyError => continue,
@@ -311,10 +413,11 @@
                     },
                 }
             }
-            Ok(V0AdvContents::NoMatchingCredentials)
+            Ok(V0AdvertisementContents::NoMatchingCredentials)
         }
-    };
+    }
 }
+
 /// Parse a NP advertisement header.
 ///
 /// This can be used on all versions of advertisements since it's the header that determines the
@@ -338,88 +441,157 @@
         _ => unreachable!(),
     }
 }
+
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub(crate) enum AdvHeader {
     V0,
     V1(V1Header),
 }
+
 /// An NP advertisement with its header parsed.
 #[derive(Debug, PartialEq, Eq)]
-pub enum DeserializedAdvertisement<'m, M: MatchedCredential<'m>> {
+pub enum DeserializedAdvertisement<'adv, M: MatchedCredential> {
     /// V0 header has all reserved bits, so there is no data to represent other than the version
     /// itself.
-    V0(V0AdvContents<'m, M>),
+    V0(V0AdvertisementContents<M>),
     /// V1 advertisement
-    V1(V1AdvContents<'m, M>),
+    V1(V1AdvertisementContents<'adv, M>),
 }
+
+impl<'adv, M: MatchedCredential> DeserializedAdvertisement<'adv, M> {
+    /// Attempts to cast this deserialized advertisement into the `V0AdvertisementContents`
+    /// variant. If the underlying advertisement is not V0, this will instead return `None`.
+    pub fn into_v0(self) -> Option<V0AdvertisementContents<M>> {
+        match self {
+            Self::V0(x) => Some(x),
+            _ => None,
+        }
+    }
+    /// Attempts to cast this deserialized advertisement into the `V1AdvertisementContents`
+    /// variant. If the underlying advertisement is not V1, this will instead return `None`.
+    pub fn into_v1(self) -> Option<V1AdvertisementContents<'adv, M>> {
+        match self {
+            Self::V1(x) => Some(x),
+            _ => None,
+        }
+    }
+}
+
 /// The contents of a deserialized and decrypted V1 advertisement.
 #[derive(Debug, PartialEq, Eq)]
-pub struct V1AdvContents<'m, M: MatchedCredential<'m>> {
-    sections: Vec<V1DeserializedSection<'m, M>>,
+pub struct V1AdvertisementContents<'adv, M: MatchedCredential> {
+    sections: Vec<V1DeserializedSection<'adv, M>>,
     invalid_sections: usize,
 }
-impl<'m, M: MatchedCredential<'m>> V1AdvContents<'m, M> {
-    fn new(sections: Vec<V1DeserializedSection<'m, M>>, invalid_sections: usize) -> Self {
+
+impl<'adv, M: MatchedCredential> V1AdvertisementContents<'adv, M> {
+    fn new(sections: Vec<V1DeserializedSection<'adv, M>>, invalid_sections: usize) -> Self {
         Self { sections, invalid_sections }
     }
+
     /// Destructures this V1 advertisement into just the sections
     /// which could be successfully deserialized and decrypted
-    pub fn into_valid_sections(self) -> Vec<V1DeserializedSection<'m, M>> {
+    pub fn into_sections(self) -> Vec<V1DeserializedSection<'adv, M>> {
         self.sections
     }
+
     /// The sections that could be successfully deserialized and decrypted
-    pub fn sections(&self) -> impl Iterator<Item = &V1DeserializedSection<M>> {
+    pub fn sections(&self) -> impl ExactSizeIterator<Item = &V1DeserializedSection<M>> {
         self.sections.iter()
     }
+
     /// The number of sections that could not be parsed or decrypted.
     pub fn invalid_sections_count(&self) -> usize {
         self.invalid_sections
     }
 }
+
 /// Advertisement content that was either already plaintext or has been decrypted.
 #[derive(Debug, PartialEq, Eq)]
-pub enum V0AdvContents<'m, M: MatchedCredential<'m>> {
+pub enum V0AdvertisementContents<M: MatchedCredential> {
     /// Contents of an originally plaintext advertisement
     Plaintext(PlaintextAdvContents),
     /// Contents that was ciphertext in the original advertisement, and has been decrypted
     /// with the credential in the [MatchedCredential]
-    Decrypted(WithMatchedCredential<'m, M, DecryptedAdvContents>),
+    Decrypted(WithMatchedCredential<M, DecryptedAdvContents>),
     /// The advertisement was encrypted, but no credentials matched
     NoMatchingCredentials,
 }
+
 /// Advertisement content that was either already plaintext or has been decrypted.
 #[derive(Debug, PartialEq, Eq)]
-pub enum V1DeserializedSection<'m, M: MatchedCredential<'m>> {
+pub enum V1DeserializedSection<'adv, M: MatchedCredential> {
     /// Section that was plaintext in the original advertisement
-    Plaintext(PlaintextSection),
+    Plaintext(PlaintextSection<'adv>),
     /// Section that was ciphertext in the original advertisement, and has been decrypted
     /// with the credential in the [MatchedCredential]
-    Decrypted(WithMatchedCredential<'m, M, DecryptedSection>),
+    Decrypted(WithMatchedCredential<M, DecryptedSection<'adv>>),
 }
-impl<'m, M> Section for V1DeserializedSection<'m, M>
+
+impl<'adv, M> Section<'adv, DataElementParseError> for V1DeserializedSection<'adv, M>
 where
-    M: MatchedCredential<'m>,
+    M: MatchedCredential,
 {
-    type Iterator<'d>  = DataElements<'d> where Self: 'd;
-    fn data_elements(&'_ self) -> Self::Iterator<'_> {
+    type Iterator = DataElementParsingIterator<'adv>;
+
+    fn iter_data_elements(&self) -> Self::Iterator {
         match self {
-            V1DeserializedSection::Plaintext(p) => p.data_elements(),
-            V1DeserializedSection::Decrypted(d) => d.contents.data_elements(),
+            V1DeserializedSection::Plaintext(p) => p.iter_data_elements(),
+            V1DeserializedSection::Decrypted(d) => d.contents.iter_data_elements(),
         }
     }
 }
-/// Decrypted advertisement content with the [MatchedCredential] from the credential that decrypted
-/// it.
-#[derive(Debug, PartialEq, Eq)]
-pub struct WithMatchedCredential<'m, M: MatchedCredential<'m>, T> {
-    matched: M,
-    contents: T,
-    // the compiler sees 'm as unused
-    marker: marker::PhantomData<&'m ()>,
+
+/// 16-byte metadata keys, as employed for metadata decryption.
+#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
+pub struct MetadataKey(pub [u8; 16]);
+
+impl AsRef<[u8]> for MetadataKey {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
 }
-impl<'m, M: MatchedCredential<'m>, T> WithMatchedCredential<'m, M, T> {
-    fn new(matched: M, contents: T) -> Self {
-        Self { matched, contents, marker: marker::PhantomData }
+
+/// Common trait to deserialized, decrypted V0 advs and V1 sections which
+/// exposes relevant data about matched identities.
+pub trait HasIdentityMatch {
+    /// The protocol version for which this advertisement
+    /// content has an identity-match.
+    type Version: ProtocolVersion;
+
+    /// Gets the decrypted plaintext version-specific
+    /// metadata key for the associated identity.
+    fn metadata_key(&self) -> <Self::Version as ProtocolVersion>::MetadataKey;
+}
+
+#[cfg(any(test, feature = "alloc"))]
+/// Type for errors from [`WithMatchedCredential#decrypt_metadata`]
+#[derive(Debug)]
+pub enum MatchedMetadataDecryptionError<M: MatchedCredential> {
+    /// Retrieving the encrypted metadata failed for one reason
+    /// or another, so we didn't get a chance to try decryption.
+    RetrievalFailed(<M as MatchedCredential>::EncryptedMetadataFetchError),
+    /// The encrypted metadata could be retrieved, but it did
+    /// not successfully decrypt against the matched identity.
+    /// This could be an indication of data corruption or
+    /// of malformed crypto on the sender-side.
+    DecryptionFailed,
+}
+
+/// Decrypted advertisement content with the [MatchedCredential] from the credential that decrypted
+/// it, along with any other information which is relevant to the identity-match.
+#[derive(Debug, PartialEq, Eq)]
+pub struct WithMatchedCredential<M: MatchedCredential, T: HasIdentityMatch> {
+    matched: M,
+    /// The 12-byte metadata nonce as derived from the key-seed HKDF
+    /// to be used for decrypting the encrypted metadata in the attached
+    /// matched-credential.
+    metadata_nonce: [u8; 12],
+    contents: T,
+}
+impl<M: MatchedCredential, T: HasIdentityMatch> WithMatchedCredential<M, T> {
+    fn new(matched: M, metadata_nonce: [u8; 12], contents: T) -> Self {
+        Self { matched, metadata_nonce, contents }
     }
     /// Credential data for the credential that decrypted the content.
     pub fn matched_credential(&self) -> &M {
@@ -429,12 +601,41 @@
     pub fn contents(&self) -> &T {
         &self.contents
     }
+
+    #[cfg(any(test, feature = "alloc"))]
+    fn decrypt_metadata_from_fetch<C: CryptoProvider>(
+        &self,
+        encrypted_metadata: &[u8],
+    ) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>> {
+        let metadata_key = self.contents.metadata_key();
+        <<T as HasIdentityMatch>::Version as ProtocolVersion>::decrypt_metadata::<C>(
+            self.metadata_nonce,
+            metadata_key,
+            encrypted_metadata,
+        )
+        .map_err(|_| MatchedMetadataDecryptionError::DecryptionFailed)
+    }
+
+    #[cfg(any(test, feature = "alloc"))]
+    /// Attempts to decrypt the encrypted metadata
+    /// associated with the matched credential
+    /// based on the details of the identity-match.
+    pub fn decrypt_metadata<C: CryptoProvider>(
+        &self,
+    ) -> Result<Vec<u8>, MatchedMetadataDecryptionError<M>> {
+        self.matched
+            .fetch_encrypted_metadata()
+            .map_err(|e| MatchedMetadataDecryptionError::RetrievalFailed(e))
+            .and_then(|x| Self::decrypt_metadata_from_fetch::<C>(self, x.as_ref()))
+    }
 }
+
 /// Data in a V1 advertisement header.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
 pub(crate) struct V1Header {
     header_byte: u8,
 }
+
 const PROTOCOL_VERSION_LEGACY: u8 = 0;
 const PROTOCOL_VERSION_EXTENDED: u8 = 1;
 
@@ -449,6 +650,14 @@
         /// [AdvDeserializationErrorDetailsHazmat] before using this field.
         details_hazmat: AdvDeserializationErrorDetailsHazmat,
     },
+    /// The given arena is not large enough to hold the decrypted data.
+    ArenaOutOfSpace,
+}
+
+impl From<ArenaOutOfSpace> for AdvDeserializationError {
+    fn from(_: ArenaOutOfSpace) -> Self {
+        Self::ArenaOutOfSpace
+    }
 }
 
 impl Debug for AdvDeserializationError {
@@ -456,6 +665,7 @@
         match self {
             AdvDeserializationError::HeaderParseError => write!(f, "HeaderParseError"),
             AdvDeserializationError::ParseError { .. } => write!(f, "ParseError"),
+            AdvDeserializationError::ArenaOutOfSpace => write!(f, "ArenaOutOfSpace"),
         }
     }
 }
@@ -474,6 +684,10 @@
     TooManyTopLevelDataElements,
     /// Must not have an identity DE inside an identity DE
     InvalidDataElementHierarchy,
+    /// Must have an identity DE
+    MissingIdentity,
+    /// Non-identity DE contents must not be empty
+    NoPublicDataElements,
 }
 
 impl From<AdvDeserializeError> for AdvDeserializationError {
@@ -503,6 +717,12 @@
                         AdvDeserializationErrorDetailsHazmat::InvalidDataElementHierarchy,
                 }
             }
+            AdvDeserializeError::MissingIdentity => AdvDeserializationError::ParseError {
+                details_hazmat: AdvDeserializationErrorDetailsHazmat::MissingIdentity,
+            },
+            AdvDeserializeError::NoPublicDataElements => AdvDeserializationError::ParseError {
+                details_hazmat: AdvDeserializationErrorDetailsHazmat::NoPublicDataElements,
+            },
         }
     }
 }
@@ -515,8 +735,6 @@
 /// The identity mode for a deserialized plaintext section or advertisement.
 #[derive(PartialEq, Eq, Debug, Clone, Copy)]
 pub enum PlaintextIdentityMode {
-    /// No identity DE was present in the section
-    None,
     /// A "Public Identity" DE was present in the section
     Public,
 }
@@ -525,10 +743,4 @@
 ///
 /// Used when serializing V0 advertisements or V1 sections.
 #[derive(Default, Debug)]
-pub struct PublicIdentity {}
-
-/// The lack of any identity information whatsoever, which is distinct from [PublicIdentity].
-///
-/// Used when serializing V0 advertisements or V1 sections.
-#[derive(Default, Debug)]
-pub struct NoIdentity {}
+pub struct PublicIdentity;
diff --git a/nearby/presence/np_adv/tests/examples_v0.rs b/nearby/presence/np_adv/tests/examples_v0.rs
index e30e12f..27ea7c5 100644
--- a/nearby/presence/np_adv/tests/examples_v0.rs
+++ b/nearby/presence/np_adv/tests/examples_v0.rs
@@ -1,5 +1,4 @@
 // Copyright 2023 Google LLC
-//
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -17,32 +16,40 @@
 use np_adv::legacy::data_elements::TxPowerDataElement;
 use np_adv::{
     credential::{
-        simple::SimpleV0Credential, source::SliceCredentialSource,
-        v0::MinimumFootprintV0CryptoMaterial,
+        book::CredentialBookBuilder,
+        v0::{V0DiscoveryCredential, V0},
+        EmptyMatchedCredential, MatchableCredential, MetadataMatchedCredential,
+        SimpleBroadcastCryptoMaterial,
     },
     de_type::*,
-    deserialize_v0_advertisement,
-    legacy::deserialize::*,
+    legacy::{deserialize::*, ShortMetadataKey},
     shared_data::*,
     *,
 };
+use serde::{Deserialize, Serialize};
 
 #[test]
 fn v0_deser_plaintext() {
-    let creds =
-        SliceCredentialSource::<SimpleV0Credential<MinimumFootprintV0CryptoMaterial, ()>>::new(&[]);
-    let adv = deserialize_v0_advertisement::<_, _, CryptoProviderImpl>(
+    let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+    let adv = deserialize_advertisement::<_, CryptoProviderImpl>(
+        deserialization_arena!(),
         &[
             0x00, // adv header
             0x03, // public identity
             0x15, 0x03, // Length 1 Tx Power DE with value 3
         ],
-        &creds,
+        &cred_book,
     )
-    .unwrap();
+    .expect("Should be a valid advertisement")
+    .into_v0()
+    .expect("Should be V0");
 
     match adv {
-        V0AdvContents::Plaintext(p) => {
+        V0AdvertisementContents::Plaintext(p) => {
             assert_eq!(PlaintextIdentityMode::Public, p.identity());
             assert_eq!(
                 vec![&PlainDataElement::TxPower(TxPowerDataElement::from(
@@ -55,14 +62,49 @@
     }
 }
 
+/// Sample contents for some encrypted identity metadata
+/// which includes just a name and an e-mail address.
+#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
+struct IdentityMetadata {
+    name: String,
+    email: String,
+}
+
+impl IdentityMetadata {
+    /// Serialize this identity metadata to a JSON byte-string.
+    fn to_bytes(&self) -> Vec<u8> {
+        serde_json::to_vec(&self).expect("serialization should always succeed")
+    }
+    /// Attempt to deserialize identity metadata from a JSON byte-string.
+    fn try_from_bytes(serialized: &[u8]) -> Option<Self> {
+        serde_json::from_slice(serialized).ok()
+    }
+}
+
 #[test]
 fn v0_deser_ciphertext() {
+    // These are kept fixed in this example for reproducibility.
+    // In practice, these should instead be derived from a cryptographically-secure
+    // random number generator.
     let key_seed = [0x11_u8; 32];
     let metadata_key: [u8; NP_LEGACY_METADATA_KEY_LEN] = [0x33; NP_LEGACY_METADATA_KEY_LEN];
+    let metadata_key = ShortMetadataKey(metadata_key);
+
+    let broadcast_cm = SimpleBroadcastCryptoMaterial::<V0>::new(key_seed, metadata_key);
 
     let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
     let metadata_key_hmac: [u8; 32] =
-        hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&metadata_key);
+        hkdf.legacy_metadata_key_hmac_key().calculate_hmac(&metadata_key.0);
+
+    // Serialize and encrypt some identity metadata (sender-side)
+    let sender_metadata =
+        IdentityMetadata { name: "Alice".to_string(), email: "alice@gmail.com".to_string() };
+    let sender_metadata_bytes = sender_metadata.to_bytes();
+    let encrypted_sender_metadata = MetadataMatchedCredential::<Vec<u8>>::encrypt_from_plaintext::<
+        _,
+        _,
+        CryptoProviderImpl,
+    >(&broadcast_cm, &sender_metadata_bytes);
 
     // output of building a packet using AdvBuilder
     let adv = &[
@@ -74,25 +116,42 @@
         0xF9,
     ];
 
-    let credentials: [SimpleV0Credential<_, [u8; 32]>; 1] = [SimpleV0Credential::new(
-        MinimumFootprintV0CryptoMaterial::new(key_seed, metadata_key_hmac),
-        key_seed,
-    )];
-    let cred_source = SliceCredentialSource::new(credentials.as_slice());
+    let discovery_credential = V0DiscoveryCredential::new(key_seed, metadata_key_hmac);
 
-    let matched = match deserialize_v0_advertisement::<_, _, CryptoProviderImpl>(adv, &cred_source)
-        .unwrap()
+    let credentials: [MatchableCredential<V0, MetadataMatchedCredential<_>>; 1] =
+        [MatchableCredential { discovery_credential, match_data: encrypted_sender_metadata }];
+
+    let cred_book = CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(
+        &credentials,
+        &[],
+    );
+
+    let matched = match deserialize_advertisement::<_, CryptoProviderImpl>(
+        deserialization_arena!(),
+        adv,
+        &cred_book,
+    )
+    .expect("Should be a valid advertisement")
+    .into_v0()
+    .expect("Should be V0")
     {
-        V0AdvContents::Decrypted(c) => c,
+        V0AdvertisementContents::Decrypted(c) => c,
         _ => panic!("this examples is ciphertext"),
     };
 
-    assert_eq!(&key_seed, matched.matched_credential().matched_data());
+    let decrypted_metadata_bytes = matched
+        .decrypt_metadata::<CryptoProviderImpl>()
+        .expect("Sender metadata should be decryptable");
+    let decrypted_metadata = IdentityMetadata::try_from_bytes(&decrypted_metadata_bytes)
+        .expect("Sender metadata should be deserializable");
+
+    assert_eq!(sender_metadata, decrypted_metadata);
+
     let decrypted = matched.contents();
 
     assert_eq!(EncryptedIdentityDataElementType::Private, decrypted.identity_type());
 
-    assert_eq!(&metadata_key, decrypted.metadata_key());
+    assert_eq!(metadata_key, decrypted.metadata_key());
 
     assert_eq!(
         vec![&PlainDataElement::TxPower(TxPowerDataElement::from(TxPower::try_from(3).unwrap())),],
diff --git a/nearby/presence/np_adv/tests/examples_v1.rs b/nearby/presence/np_adv/tests/examples_v1.rs
index fbda763..bf633e5 100644
--- a/nearby/presence/np_adv/tests/examples_v1.rs
+++ b/nearby/presence/np_adv/tests/examples_v1.rs
@@ -15,38 +15,47 @@
 use crypto_provider::{CryptoProvider, CryptoRng};
 use crypto_provider_default::CryptoProviderImpl;
 use np_adv::extended::data_elements::TxPowerDataElement;
-use np_adv::extended::serialize::SingleTypeDataElement;
+use np_adv::extended::serialize::{AdvertisementType, PublicSectionEncoder, SingleTypeDataElement};
+use np_adv::extended::NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT;
 use np_adv::shared_data::TxPower;
 use np_adv::{
     credential::{
-        simple::SimpleV1Credential, source::SliceCredentialSource,
-        v1::MinimumFootprintV1CryptoMaterial,
+        book::CredentialBookBuilder,
+        v1::{SimpleSignedBroadcastCryptoMaterial, V1DiscoveryCredential, V1},
+        EmptyMatchedCredential, MatchableCredential, MetadataMatchedCredential,
     },
     de_type::*,
-    deserialize_v1_advertisement,
     extended::{
         deserialize::{Section, VerificationMode},
-        serialize::{AdvBuilder, SignedEncrypted},
-        NP_V1_ADV_MAX_SECTION_COUNT,
+        serialize::{AdvBuilder, SignedEncryptedSectionEncoder},
     },
     PlaintextIdentityMode, *,
 };
 use np_hkdf::v1_salt;
+use serde::{Deserialize, Serialize};
 
 #[test]
 fn v1_deser_plaintext() {
-    let mut adv_builder = AdvBuilder::new();
-    let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    let mut section_builder = adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
     section_builder
         .add_de(|_salt| TxPowerDataElement::from(TxPower::try_from(6).unwrap()))
         .unwrap();
     section_builder.add_to_advertisement();
     let adv = adv_builder.into_advertisement();
 
-    let creds =
-        SliceCredentialSource::<SimpleV1Credential<MinimumFootprintV1CryptoMaterial, ()>>::new(&[]);
+    let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+
+    let arena = deserialization_arena!();
     let contents =
-        deserialize_v1_advertisement::<_, _, CryptoProviderImpl>(adv.as_slice(), &creds).unwrap();
+        deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book)
+            .expect("Should be a valid advertisemement")
+            .into_v1()
+            .expect("Should be V1");
 
     assert_eq!(0, contents.invalid_sections_count());
 
@@ -59,7 +68,7 @@
         _ => panic!("this is a plaintext adv"),
     };
     assert_eq!(PlaintextIdentityMode::Public, section.identity());
-    let data_elements = section.data_elements().collect::<Vec<_>>();
+    let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
     assert_eq!(1, data_elements.len());
 
     let de = &data_elements[0];
@@ -68,24 +77,61 @@
     assert_eq!(&[6], de.contents());
 }
 
+/// Sample contents for some encrypted identity metadata
+/// which consists of a UUID together with a display name
+/// and a general location.
+#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
+struct IdentityMetadata {
+    uuid: String,
+    display_name: String,
+    location: String,
+}
+
+impl IdentityMetadata {
+    /// Serialize this identity metadata to a MsgPack byte-string.
+    fn to_bytes(&self) -> Vec<u8> {
+        rmp_serde::to_vec(&self).expect("serialization should always succeed")
+    }
+    /// Attempt to deserialize identity metadata from a MsgPack byte-string.
+    fn try_from_bytes(serialized: &[u8]) -> Option<Self> {
+        rmp_serde::from_slice(serialized).ok()
+    }
+}
+
 #[test]
 fn v1_deser_ciphertext() {
     // identity material
     let mut rng = <CryptoProviderImpl as CryptoProvider>::CryptoRng::new();
     let metadata_key: [u8; 16] = rng.gen();
+    let metadata_key = MetadataKey(metadata_key);
     let key_pair = np_ed25519::KeyPair::<CryptoProviderImpl>::generate();
     let key_seed = rng.gen();
     let hkdf = np_hkdf::NpKeySeedHkdf::<CryptoProviderImpl>::new(&key_seed);
 
+    let broadcast_cm =
+        SimpleSignedBroadcastCryptoMaterial::new(key_seed, metadata_key, key_pair.private_key());
+
+    // Serialize and encrypt some identity metadata (sender-side)
+    let sender_metadata = IdentityMetadata {
+        uuid: "378845e1-2616-420d-86f5-674177a7504d".to_string(),
+        display_name: "Alice".to_string(),
+        location: "Wonderland".to_string(),
+    };
+    let sender_metadata_bytes = sender_metadata.to_bytes();
+    let encrypted_sender_metadata = MetadataMatchedCredential::<Vec<u8>>::encrypt_from_plaintext::<
+        _,
+        _,
+        CryptoProviderImpl,
+    >(&broadcast_cm, &sender_metadata_bytes);
+
     // prepare advertisement
-    let mut adv_builder = AdvBuilder::new();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Encrypted);
+
     let mut section_builder = adv_builder
-        .section_builder(SignedEncrypted::new_random_salt(
+        .section_builder(SignedEncryptedSectionEncoder::<CryptoProviderImpl>::new_random_salt(
             &mut rng,
             EncryptedIdentityDataElementType::Private,
-            &metadata_key,
-            &key_pair,
-            &hkdf,
+            &broadcast_cm,
         ))
         .unwrap();
     section_builder
@@ -94,37 +140,50 @@
     section_builder.add_to_advertisement();
     let adv = adv_builder.into_advertisement();
 
-    let cred_array: [SimpleV1Credential<_, [u8; 32]>; 1] = [SimpleV1Credential::new(
-        MinimumFootprintV1CryptoMaterial::new(
-            key_seed,
-            [0; 32], // Zeroing out MIC HMAC, since it's unused in examples here.
-            hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key),
-            key_pair.public(),
-        ),
+    let discovery_credential = V1DiscoveryCredential::new(
         key_seed,
-    )];
-    let creds = SliceCredentialSource::new(&cred_array);
+        [0; 32], // Zeroing out MIC HMAC, since it's unused in examples here.
+        hkdf.extended_signed_metadata_key_hmac_key().calculate_hmac(&metadata_key.0),
+        key_pair.public(),
+    );
+
+    let credentials: [MatchableCredential<V1, MetadataMatchedCredential<_>>; 1] =
+        [MatchableCredential { discovery_credential, match_data: encrypted_sender_metadata }];
+    let cred_book = CredentialBookBuilder::build_cached_slice_book::<0, 0, CryptoProviderImpl>(
+        &[],
+        &credentials,
+    );
+    let arena = deserialization_arena!();
     let contents =
-        deserialize_v1_advertisement::<_, _, CryptoProviderImpl>(adv.as_slice(), &creds).unwrap();
+        deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book)
+            .expect("Should be a valid advertisement")
+            .into_v1()
+            .expect("Should be V1");
 
     assert_eq!(0, contents.invalid_sections_count());
 
     let sections = contents.sections().collect::<Vec<_>>();
     assert_eq!(1, sections.len());
 
-    let matched_credential = match &sections[0] {
+    let matched: &WithMatchedCredential<_, _> = match &sections[0] {
         V1DeserializedSection::Decrypted(d) => d,
         _ => panic!("this is a ciphertext adv"),
     };
 
-    assert_eq!(&key_seed, matched_credential.matched_credential().matched_data());
-    let section = matched_credential.contents();
+    let decrypted_metadata_bytes = matched
+        .decrypt_metadata::<CryptoProviderImpl>()
+        .expect("Sender metadata should be decryptable");
+    let decrypted_metadata = IdentityMetadata::try_from_bytes(&decrypted_metadata_bytes)
+        .expect("Sender metadata should be deserializable");
+    assert_eq!(sender_metadata, decrypted_metadata);
+
+    let section = matched.contents();
 
     assert_eq!(EncryptedIdentityDataElementType::Private, section.identity_type());
     assert_eq!(VerificationMode::Signature, section.verification_mode());
-    assert_eq!(&metadata_key, section.metadata_key());
+    assert_eq!(metadata_key, section.metadata_key());
 
-    let data_elements = section.data_elements().collect::<Vec<_>>();
+    let data_elements = section.iter_data_elements().collect::<Result<Vec<_>, _>>().unwrap();
     assert_eq!(1, data_elements.len());
 
     let de = &data_elements[0];
@@ -135,12 +194,16 @@
 
 #[test]
 fn v1_deser_no_section() {
-    let adv_builder = AdvBuilder::new();
+    let adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
     let adv = adv_builder.into_advertisement();
-    let creds =
-        SliceCredentialSource::<SimpleV1Credential<MinimumFootprintV1CryptoMaterial, ()>>::new(&[]);
+    let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+    let arena = deserialization_arena!();
     let v1_deserialize_error =
-        deserialize_v1_advertisement::<_, _, CryptoProviderImpl>(adv.as_slice(), &creds)
+        deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book)
             .expect_err(" Expected an error");
     assert_eq!(
         v1_deserialize_error,
@@ -152,9 +215,10 @@
 
 #[test]
 fn v1_deser_plaintext_over_max_sections() {
-    let mut adv_builder = AdvBuilder::new();
-    for _ in 0..NP_V1_ADV_MAX_SECTION_COUNT {
-        let mut section_builder = adv_builder.section_builder(PublicIdentity::default()).unwrap();
+    let mut adv_builder = AdvBuilder::new(AdvertisementType::Plaintext);
+    for _ in 0..NP_V1_ADV_MAX_PUBLIC_SECTION_COUNT {
+        let mut section_builder =
+            adv_builder.section_builder(PublicSectionEncoder::default()).unwrap();
         section_builder
             .add_de(|_salt| TxPowerDataElement::from(TxPower::try_from(7).unwrap()))
             .unwrap();
@@ -169,10 +233,14 @@
         ]
         .as_slice(),
     );
-    let creds =
-        SliceCredentialSource::<SimpleV1Credential<MinimumFootprintV1CryptoMaterial, ()>>::new(&[]);
+    let cred_book = CredentialBookBuilder::<EmptyMatchedCredential>::build_cached_slice_book::<
+        0,
+        0,
+        CryptoProviderImpl,
+    >(&[], &[]);
+    let arena = deserialization_arena!();
     assert_eq!(
-        deserialize_v1_advertisement::<_, _, CryptoProviderImpl>(adv.as_slice(), &creds)
+        deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv.as_slice(), &cred_book)
             .unwrap_err(),
         AdvDeserializationError::ParseError {
             details_hazmat: AdvDeserializationErrorDetailsHazmat::AdvertisementDeserializeError
diff --git a/nearby/presence/np_c_ffi/Cargo.lock b/nearby/presence/np_c_ffi/Cargo.lock
index ca23800..f55cf29 100644
--- a/nearby/presence/np_c_ffi/Cargo.lock
+++ b/nearby/presence/np_c_ffi/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -40,23 +54,6 @@
 ]
 
 [[package]]
-name = "ahash"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
-dependencies = [
- "cfg-if",
- "once_cell",
- "version_check",
-]
-
-[[package]]
-name = "allocator-api2"
-version = "0.2.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
-
-[[package]]
 name = "array_ref"
 version = "0.1.0"
 
@@ -70,7 +67,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi 0.1.19",
+ "hermit-abi",
  "libc",
  "winapi",
 ]
@@ -88,12 +85,24 @@
 checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
 
 [[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
 name = "bitflags"
 version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
+name = "bitflags"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+
+[[package]]
 name = "block-buffer"
 version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -112,10 +121,21 @@
 ]
 
 [[package]]
+name = "bssl-crypto"
+version = "0.1.0"
+dependencies = [
+ "bssl-sys",
+]
+
+[[package]]
+name = "bssl-sys"
+version = "0.1.0"
+
+[[package]]
 name = "bytes"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
 
 [[package]]
 name = "cbc"
@@ -146,12 +166,6 @@
 ]
 
 [[package]]
-name = "cc"
-version = "1.0.79"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
-
-[[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -174,7 +188,7 @@
 checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 dependencies = [
  "atty",
- "bitflags",
+ "bitflags 1.3.2",
  "clap_lex",
  "indexmap",
  "strsim",
@@ -193,24 +207,24 @@
 
 [[package]]
 name = "const-oid"
-version = "0.9.3"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc"
+checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.9"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "crypto-bigint"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15"
+checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
 dependencies = [
  "generic-array",
  "rand_core",
@@ -232,6 +246,17 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "crypto_provider_boringssl"
+version = "0.1.0"
+dependencies = [
+ "bssl-crypto",
+ "crypto_provider",
+]
 
 [[package]]
 name = "crypto_provider_default"
@@ -239,6 +264,7 @@
 dependencies = [
  "cfg-if",
  "crypto_provider",
+ "crypto_provider_boringssl",
  "crypto_provider_rustcrypto",
 ]
 
@@ -248,6 +274,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -277,9 +304,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -289,24 +316,25 @@
  "platforms",
  "rustc_version",
  "subtle",
+ "zeroize",
 ]
 
 [[package]]
 name = "curve25519-dalek-derive"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.38",
 ]
 
 [[package]]
 name = "der"
-version = "0.7.7"
+version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
 dependencies = [
  "const-oid",
  "zeroize",
@@ -325,30 +353,33 @@
 
 [[package]]
 name = "ed25519"
-version = "2.2.1"
+version = "2.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
 dependencies = [
+ "pkcs8",
  "signature",
 ]
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
  "rand_core",
+ "serde",
  "sha2",
+ "zeroize",
 ]
 
 [[package]]
 name = "elliptic-curve"
-version = "0.13.5"
+version = "0.13.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
+checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
 dependencies = [
  "base16ct",
  "crypto-bigint",
@@ -365,33 +396,19 @@
 
 [[package]]
 name = "errno"
-version = "0.3.1"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
 dependencies = [
- "errno-dragonfly",
  "libc",
  "windows-sys",
 ]
 
 [[package]]
-name = "errno-dragonfly"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
-dependencies = [
- "cc",
- "libc",
-]
-
-[[package]]
 name = "fastrand"
-version = "1.9.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
-dependencies = [
- "instant",
-]
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
 
 [[package]]
 name = "ff"
@@ -405,9 +422,9 @@
 
 [[package]]
 name = "fiat-crypto"
-version = "0.1.20"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
+checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
 
 [[package]]
 name = "generic-array"
@@ -432,6 +449,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -447,10 +474,7 @@
 version = "0.1.0"
 dependencies = [
  "crypto_provider",
- "hashbrown 0.14.0",
- "lock_api",
- "portable-atomic",
- "spin 0.9.8",
+ "lock_adapter",
 ]
 
 [[package]]
@@ -460,16 +484,6 @@
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
-name = "hashbrown"
-version = "0.14.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
-dependencies = [
- "ahash",
- "allocator-api2",
-]
-
-[[package]]
 name = "heck"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -485,12 +499,6 @@
 ]
 
 [[package]]
-name = "hermit-abi"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
-
-[[package]]
 name = "hkdf"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -515,7 +523,7 @@
 checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 dependencies = [
  "autocfg",
- "hashbrown 0.12.3",
+ "hashbrown",
 ]
 
 [[package]]
@@ -529,30 +537,10 @@
 ]
 
 [[package]]
-name = "instant"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "io-lifetimes"
-version = "1.0.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
-dependencies = [
- "hermit-abi 0.3.2",
- "libc",
- "windows-sys",
-]
-
-[[package]]
 name = "itoa"
-version = "1.0.8"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
 
 [[package]]
 name = "lazy_static"
@@ -560,7 +548,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 dependencies = [
- "spin 0.5.2",
+ "spin",
 ]
 
 [[package]]
@@ -592,37 +580,31 @@
 
 [[package]]
 name = "libc"
-version = "0.2.147"
+version = "0.2.149"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
-
-[[package]]
-name = "lock_api"
 version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
-dependencies = [
- "autocfg",
- "scopeguard",
-]
+checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"
+
+[[package]]
+name = "lock_adapter"
+version = "0.1.0"
 
 [[package]]
 name = "log"
-version = "0.4.19"
+version = "0.4.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
 
 [[package]]
 name = "memchr"
-version = "2.5.0"
+version = "2.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
 
 [[package]]
 name = "minimal-lexical"
@@ -652,7 +634,6 @@
  "nom",
  "np_ed25519",
  "np_hkdf",
- "rand",
  "sink",
  "strum",
  "strum_macros",
@@ -665,9 +646,8 @@
 version = "0.1.0"
 dependencies = [
  "cbindgen",
- "crypto_provider_default",
+ "lock_adapter",
  "np_ffi_core",
- "spin 0.9.8",
 ]
 
 [[package]]
@@ -688,8 +668,9 @@
  "crypto_provider",
  "crypto_provider_default",
  "handle_map",
+ "lazy_static",
+ "lock_adapter",
  "np_adv",
- "spin 0.9.8",
 ]
 
 [[package]]
@@ -702,12 +683,6 @@
 ]
 
 [[package]]
-name = "once_cell"
-version = "1.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
-
-[[package]]
 name = "opaque-debug"
 version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -715,9 +690,9 @@
 
 [[package]]
 name = "os_str_bytes"
-version = "6.5.1"
+version = "6.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
+checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
 
 [[package]]
 name = "p256"
@@ -730,10 +705,20 @@
 ]
 
 [[package]]
-name = "platforms"
-version = "3.0.2"
+name = "pkcs8"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "platforms"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0"
 
 [[package]]
 name = "polyval"
@@ -748,12 +733,6 @@
 ]
 
 [[package]]
-name = "portable-atomic"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794"
-
-[[package]]
 name = "ppv-lite86"
 version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -770,18 +749,18 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.63"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
+checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.29"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
 dependencies = [
  "proc-macro2",
 ]
@@ -792,6 +771,8 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
+ "libc",
+ "rand_chacha",
  "rand_core",
 ]
 
@@ -816,11 +797,11 @@
 
 [[package]]
 name = "redox_syscall"
-version = "0.3.5"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
 ]
 
 [[package]]
@@ -834,13 +815,12 @@
 
 [[package]]
 name = "rustix"
-version = "0.37.23"
+version = "0.38.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
+checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
 dependencies = [
- "bitflags",
+ "bitflags 2.4.1",
  "errno",
- "io-lifetimes",
  "libc",
  "linux-raw-sys",
  "windows-sys",
@@ -848,27 +828,21 @@
 
 [[package]]
 name = "rustversion"
-version = "1.0.13"
+version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
 
 [[package]]
 name = "ryu"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
-
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 
 [[package]]
 name = "sec1"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
 dependencies = [
  "base16ct",
  "der",
@@ -879,35 +853,35 @@
 
 [[package]]
 name = "semver"
-version = "1.0.17"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
+checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
 
 [[package]]
 name = "serde"
-version = "1.0.166"
+version = "1.0.190"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
+checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.166"
+version = "1.0.190"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
+checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.23",
+ "syn 2.0.38",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.100"
+version = "1.0.108"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
 dependencies = [
  "itoa",
  "ryu",
@@ -916,9 +890,9 @@
 
 [[package]]
 name = "sha2"
-version = "0.10.7"
+version = "0.10.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -945,12 +919,13 @@
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
 [[package]]
-name = "spin"
-version = "0.9.8"
+name = "spki"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
 dependencies = [
- "lock_api",
+ "base64ct",
+ "der",
 ]
 
 [[package]]
@@ -961,21 +936,21 @@
 
 [[package]]
 name = "strum"
-version = "0.24.1"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
 
 [[package]]
 name = "strum_macros"
-version = "0.24.3"
+version = "0.25.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
 dependencies = [
  "heck",
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 1.0.109",
+ "syn 2.0.38",
 ]
 
 [[package]]
@@ -997,9 +972,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.23"
+version = "2.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
+checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1008,11 +983,10 @@
 
 [[package]]
 name = "tempfile"
-version = "3.6.0"
+version = "3.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
+checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
 dependencies = [
- "autocfg",
  "cfg-if",
  "fastrand",
  "redox_syscall",
@@ -1022,9 +996,9 @@
 
 [[package]]
 name = "termcolor"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
 dependencies = [
  "winapi-util",
 ]
@@ -1052,15 +1026,15 @@
 
 [[package]]
 name = "typenum"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.10"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
 name = "universal-hash"
@@ -1102,9 +1076,9 @@
 
 [[package]]
 name = "winapi-util"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
 dependencies = [
  "winapi",
 ]
@@ -1126,9 +1100,9 @@
 
 [[package]]
 name = "windows-targets"
-version = "0.48.1"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
 dependencies = [
  "windows_aarch64_gnullvm",
  "windows_aarch64_msvc",
@@ -1141,51 +1115,51 @@
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.48.0"
+version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/presence/np_c_ffi/Cargo.toml b/nearby/presence/np_c_ffi/Cargo.toml
index 1dd9f32..86bbd53 100644
--- a/nearby/presence/np_c_ffi/Cargo.toml
+++ b/nearby/presence/np_c_ffi/Cargo.toml
@@ -5,14 +5,27 @@
 publish = false
 
 [dependencies]
-# TODO: We need to make this configurable for this crate and for np_ffi below it.
-crypto_provider_default = {path = "../../crypto/crypto_provider_default", features = ["rustcrypto"]}
-np_ffi_core = {path = "../np_ffi_core"}
-spin = "0.9.8"
+np_ffi_core = { path = "../np_ffi_core" }
+lock_adapter = {path = "../../util/lock_adapter"}
 
 [build-dependencies]
 cbindgen = "0.24.5"
 
+[features]
+default = ["rustcrypto"]
+rustcrypto = ["np_ffi_core/rustcrypto"]
+boringssl = ["np_ffi_core/boringssl"]
+
 [lib]
 # boringssl and bssl-sys are built as a static lib, so we need to as well
-crate-type = ["staticlib"]
\ No newline at end of file
+crate-type = ["staticlib"]
+
+# build profile optimized for size
+[profile.release-min-size]
+inherits = "release"
+panic = "abort"
+codegen-units = 1
+lto = true
+# z optimizes for size
+opt-level = "z"
+strip = true
\ No newline at end of file
diff --git a/nearby/presence/np_c_ffi/deny.toml b/nearby/presence/np_c_ffi/deny.toml
new file mode 100644
index 0000000..ca51e11
--- /dev/null
+++ b/nearby/presence/np_c_ffi/deny.toml
@@ -0,0 +1,213 @@
+# This template contains all of the possible sections and their default values
+
+# Note that all fields that take a lint level have these possible values:
+# * deny - An error will be produced and the check will fail
+# * warn - A warning will be produced, but the check will not fail
+# * allow - No warning or error will be produced, though in some cases a note
+# will be
+
+# The values provided in this template are the default values that will be used
+# when any section or field is not specified in your own configuration
+
+# If 1 or more target triples (and optionally, target_features) are specified,
+# only the specified targets will be checked when running `cargo deny check`.
+# This means, if a particular package is only ever used as a target specific
+# dependency, such as, for example, the `nix` crate only being used via the
+# `target_family = "unix"` configuration, that only having windows targets in
+# this list would mean the nix crate, as well as any of its exclusive
+# dependencies not shared by any other crates, would be ignored, as the target
+# list here is effectively saying which targets you are building for.
+targets = [
+    # The triple can be any string, but only the target triples built in to
+    # rustc (as of 1.40) can be checked against actual config expressions
+    #{ triple = "x86_64-unknown-linux-musl" },
+    # You can also specify which target_features you promise are enabled for a
+    # particular target. target_features are currently not validated against
+    # the actual valid features supported by the target architecture.
+    #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
+]
+
+# This section is considered when running `cargo deny check advisories`
+# More documentation for the advisories section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
+[advisories]
+# The path where the advisory database is cloned/fetched into
+db-path = "~/.cargo/advisory-db"
+# The url(s) of the advisory databases to use
+db-urls = ["https://github.com/rustsec/advisory-db"]
+# The lint level for security vulnerabilities
+vulnerability = "deny"
+# The lint level for unmaintained crates
+unmaintained = "warn"
+# The lint level for crates that have been yanked from their source registry
+yanked = "warn"
+# The lint level for crates with security notices. Note that as of
+# 2019-12-17 there are no security notice advisories in
+# https://github.com/rustsec/advisory-db
+notice = "warn"
+# A list of advisory IDs to ignore. Note that ignored advisories will still
+# output a note when they are encountered.
+ignore = [
+    #"RUSTSEC-0000-0000",
+]
+# Threshold for security vulnerabilities, any vulnerability with a CVSS score
+# lower than the range specified will be ignored. Note that ignored advisories
+# will still output a note when they are encountered.
+# * None - CVSS Score 0.0
+# * Low - CVSS Score 0.1 - 3.9
+# * Medium - CVSS Score 4.0 - 6.9
+# * High - CVSS Score 7.0 - 8.9
+# * Critical - CVSS Score 9.0 - 10.0
+#severity-threshold =
+
+# If this is true, then cargo deny will use the git executable to fetch advisory database.
+# If this is false, then it uses a built-in git library.
+# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
+# See Git Authentication for more information about setting up git authentication.
+#git-fetch-with-cli = true
+
+# This section is considered when running `cargo deny check licenses`
+# More documentation for the licenses section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
+[licenses]
+# The lint level for crates which do not have a detectable license
+unlicensed = "deny"
+# List of explicitly allowed licenses
+# See https://spdx.org/licenses/ for list of possible licenses
+# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
+allow = [
+    "MIT",
+    "Apache-2.0",
+    "BSD-3-Clause",
+    "Unicode-DFS-2016",
+]
+# List of explicitly disallowed licenses
+# See https://spdx.org/licenses/ for list of possible licenses
+# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
+deny = [
+    #"Nokia",
+]
+# Lint level for licenses considered copyleft
+copyleft = "warn"
+# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
+# * both - The license will be approved if it is both OSI-approved *AND* FSF
+# * either - The license will be approved if it is either OSI-approved *OR* FSF
+# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
+# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
+# * neither - This predicate is ignored and the default lint level is used
+allow-osi-fsf-free = "neither"
+# Lint level used when no other predicates are matched
+# 1. License isn't in the allow or deny lists
+# 2. License isn't copyleft
+# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
+default = "deny"
+# The confidence threshold for detecting a license from license text.
+# The higher the value, the more closely the license text must be to the
+# canonical license text of a valid SPDX license file.
+# [possible values: any between 0.0 and 1.0].
+confidence-threshold = 0.8
+# Allow 1 or more licenses on a per-crate basis, so that particular licenses
+# aren't accepted for every possible crate as with the normal allow list
+exceptions = [
+    # Each entry is the crate and version constraint, and its specific allow
+    # list
+    #{ allow = ["Zlib"], name = "adler32", version = "*" },
+]
+
+# Some crates don't have (easily) machine readable licensing information,
+# adding a clarification entry for it allows you to manually specify the
+# licensing information
+#[[licenses.clarify]]
+# The name of the crate the clarification applies to
+#name = "ring"
+# The optional version constraint for the crate
+#version = "*"
+# The SPDX expression for the license requirements of the crate
+#expression = "MIT AND ISC AND OpenSSL"
+# One or more files in the crate's source used as the "source of truth" for
+# the license expression. If the contents match, the clarification will be used
+# when running the license check, otherwise the clarification will be ignored
+# and the crate will be checked normally, which may produce warnings or errors
+# depending on the rest of your configuration
+#license-files = [
+    # Each entry is a crate relative path, and the (opaque) hash of its contents
+    #{ path = "LICENSE", hash = 0xbd0eed23 }
+#]
+
+[[licenses.clarify]]
+name = "ring"
+version = "*"
+expression = "MIT AND ISC AND OpenSSL"
+license-files = [
+    # Each entry is a crate relative path, and the (opaque) hash of its contents
+    { path = "LICENSE", hash = 0xbd0eed23 }
+]
+
+[licenses.private]
+# If true, ignores workspace crates that aren't published, or are only
+# published to private registries.
+# To see how to mark a crate as unpublished (to the official registry),
+# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
+ignore = true
+# One or more private registries that you might publish crates to, if a crate
+# is only published to private registries, and ignore is true, the crate will
+# not have its license(s) checked
+registries = [
+    #"https://sekretz.com/registry
+]
+
+# This section is considered when running `cargo deny check bans`.
+# More documentation about the 'bans' section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
+[bans]
+# Lint level for when multiple versions of the same crate are detected
+multiple-versions = "allow"
+# Lint level for when a crate version requirement is `*`
+wildcards = "allow"
+# The graph highlighting used when creating dotgraphs for crates
+# with multiple versions
+# * lowest-version - The path to the lowest versioned duplicate is highlighted
+# * simplest-path - The path to the version with the fewest edges is highlighted
+# * all - Both lowest-version and simplest-path are used
+highlight = "all"
+# List of crates that are allowed. Use with care!
+allow = [
+    #{ name = "ansi_term", version = "=0.11.0" },
+]
+# List of crates to deny
+deny = [
+    # Each entry the name of a crate and a version range. If version is
+    # not specified, all versions will be matched.
+    #{ name = "ansi_term", version = "=0.11.0" },
+    #
+    # Wrapper crates can optionally be specified to allow the crate when it
+    # is a direct dependency of the otherwise banned crate
+    #{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
+]
+# Certain crates/versions that will be skipped when doing duplicate detection.
+skip = [
+    #{ name = "ansi_term", version = "=0.11.0" },
+]
+# Similarly to `skip` allows you to skip certain crates during duplicate
+# detection. Unlike skip, it also includes the entire tree of transitive
+# dependencies starting at the specified crate, up to a certain depth, which is
+# by default infinite
+skip-tree = [
+    #{ name = "ansi_term", version = "=0.11.0", depth = 20 },
+]
+
+# This section is considered when running `cargo deny check sources`.
+# More documentation about the 'sources' section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
+[sources]
+# Lint level for what to happen when a crate from a crate registry that is not
+# in the allow list is encountered
+unknown-registry = "warn"
+# Lint level for what to happen when a crate from a git repository that is not
+# in the allow list is encountered
+unknown-git = "warn"
+# List of URLs for allowed crate registries. Defaults to the crates.io index
+# if not specified. If it is specified but empty, no registries are allowed.
+allow-registry = ["https://github.com/rust-lang/crates.io-index"]
+# List of URLs for allowed Git repositories
+allow-git = []
\ No newline at end of file
diff --git a/nearby/presence/np_c_ffi/src/deserialize/v0.rs b/nearby/presence/np_c_ffi/src/deserialize/v0.rs
index 877cee5..24c4c2a 100644
--- a/nearby/presence/np_c_ffi/src/deserialize/v0.rs
+++ b/nearby/presence/np_c_ffi/src/deserialize/v0.rs
@@ -49,7 +49,7 @@
 pub extern "C" fn np_ffi_LegibleDeserializedV0Advertisement_into_payload(
     adv: LegibleDeserializedV0Advertisement,
 ) -> V0Payload {
-    adv.into_payload()
+    adv.payload()
 }
 
 /// Gets just the identity information associated with a `LegibleDeserializedV0Advertisement`.
@@ -57,7 +57,7 @@
 pub extern "C" fn np_ffi_LegibleDeserializedV0Advertisement_into_identity(
     adv: LegibleDeserializedV0Advertisement,
 ) -> DeserializedV0Identity {
-    adv.into_identity()
+    adv.identity()
 }
 
 /// Deallocates any internal data of a `LegibleDeserializedV0Advertisement`
diff --git a/nearby/presence/np_c_ffi/src/lib.rs b/nearby/presence/np_c_ffi/src/lib.rs
index bd475ab..0dd6d33 100644
--- a/nearby/presence/np_c_ffi/src/lib.rs
+++ b/nearby/presence/np_c_ffi/src/lib.rs
@@ -14,17 +14,15 @@
 
 //! NP Rust C FFI
 
-#![cfg_attr(not(test), no_std)]
-#![allow(dead_code)]
-extern crate alloc;
-extern crate core;
-
 pub mod credentials;
 pub mod deserialize;
 
+use lock_adapter::std::RwLock;
+use lock_adapter::RwLock as _;
+
 /// Structure for categorized reasons for why a NP C FFI call may
 /// be panicking.
-#[derive(Clone, Copy)]
+#[derive(Clone, Copy, Debug)]
 #[repr(u8)]
 pub enum PanicReason {
     /// Some enum cast to a variant failed. Utilized
@@ -45,7 +43,6 @@
 
 /// Structure which maintains information about the panic-handler
 /// for panicking calls in the NP C FFI.
-#[derive(Debug)]
 struct PanicHandler {
     /// Optional function-pointer to client-specified panic behavior.
     handler: Option<unsafe extern "C" fn(PanicReason) -> ()>,
@@ -81,27 +78,18 @@
         }
         Self::system_handler(panic_reason)
     }
-    #[cfg(feature = "std")]
+
     fn system_handler(panic_reason: PanicReason) -> ! {
-        eprintln!("NP FFI Panicked: {:?}", panic_reason);
+        std::eprintln!("NP FFI Panicked: {:?}", panic_reason);
         let backtrace = std::backtrace::Backtrace::capture();
-        eprintln!("Stack trace: {}", backtrace);
-        std::process::abort!();
-    }
-    #[cfg(not(feature = "std"))]
-    #[allow(clippy::empty_loop)]
-    fn system_handler(_: PanicReason) -> ! {
-        // Looping is the only platform-independent thing
-        // that we can really do in this scenario.
-        // (Even clippy's explanation for the empty-loop
-        // lint mentions platform-specific intrinsics
-        // as being the only true way to avoid this
-        // in a no_std environment.)
-        loop {}
+        std::eprintln!("Stack trace: {}", backtrace);
+        std::process::abort()
     }
 }
 
-static PANIC_HANDLER: spin::RwLock<PanicHandler> = spin::RwLock::new(PanicHandler::new());
+//TODO: use a std library RwLock if we have that available, spin is only needed for no_std and
+// won't be as performant
+static PANIC_HANDLER: RwLock<PanicHandler> = RwLock::new(PanicHandler::new());
 
 pub(crate) fn panic(reason: PanicReason) -> ! {
     PANIC_HANDLER.read().panic(reason)
diff --git a/nearby/presence/np_cpp_ffi/fuzz/CMakeLists.txt b/nearby/presence/np_cpp_ffi/fuzz/CMakeLists.txt
index 17f2f1f..0a1c088 100644
--- a/nearby/presence/np_cpp_ffi/fuzz/CMakeLists.txt
+++ b/nearby/presence/np_cpp_ffi/fuzz/CMakeLists.txt
@@ -16,6 +16,7 @@
 
 # libfuzzer needs clang
 SET(CMAKE_C_COMPILER "clang")
+SET(CMAKE_CXX_COMPILER "clang++")
 
 add_compile_definitions("CORPUS_DIR=${CMAKE_CURRENT_SOURCE_DIR}/corpus")
 file(COPY corpus DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
diff --git a/nearby/presence/np_cpp_ffi/sample/CMakeLists.txt b/nearby/presence/np_cpp_ffi/sample/CMakeLists.txt
index da33258..3987edd 100644
--- a/nearby/presence/np_cpp_ffi/sample/CMakeLists.txt
+++ b/nearby/presence/np_cpp_ffi/sample/CMakeLists.txt
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-add_executable(np_cpp_sample main.cpp)
+add_executable(np_cpp_sample main.cc)
 
 target_link_libraries(np_cpp_sample nearby_protocol)
 
diff --git a/nearby/presence/np_cpp_ffi/sample/main.cpp b/nearby/presence/np_cpp_ffi/sample/main.cc
similarity index 98%
rename from nearby/presence/np_cpp_ffi/sample/main.cpp
rename to nearby/presence/np_cpp_ffi/sample/main.cc
index 114ed64..a18b996 100644
--- a/nearby/presence/np_cpp_ffi/sample/main.cpp
+++ b/nearby/presence/np_cpp_ffi/sample/main.cc
@@ -132,7 +132,7 @@
     std::cout << "AssertFailed \n";
     break;
   }
-  case np_ffi::internal::PanicReason::InvalidActionBits: {
+  case nearby_protocol::PanicReason::InvalidActionBits: {
     std::cout << "InvalidActionBits \n";
     break;
   }
diff --git a/nearby/presence/np_cpp_ffi/shared/shared_test_util.cc b/nearby/presence/np_cpp_ffi/shared/shared_test_util.cc
index 27e98a6..67ca828 100644
--- a/nearby/presence/np_cpp_ffi/shared/shared_test_util.cc
+++ b/nearby/presence/np_cpp_ffi/shared/shared_test_util.cc
@@ -14,6 +14,9 @@
 
 #include "nearby_protocol.h"
 
+#include <cstdlib>
+#include <cstddef> // IWYU pragma: keep
+
 std::string PanicReasonToString(nearby_protocol::PanicReason reason) {
   switch (reason) {
   case nearby_protocol::PanicReason::EnumCastFailed: {
@@ -22,7 +25,7 @@
   case nearby_protocol::PanicReason::AssertFailed: {
     return "AssertFailed";
   }
-  case np_ffi::internal::PanicReason::InvalidActionBits: {
+  case nearby_protocol::PanicReason::InvalidActionBits: {
     return "InvalidActionBits";
   }
   }
diff --git a/nearby/presence/np_cpp_ffi/shared/shared_test_util.h b/nearby/presence/np_cpp_ffi/shared/shared_test_util.h
index 52c5363..22d1bb3 100644
--- a/nearby/presence/np_cpp_ffi/shared/shared_test_util.h
+++ b/nearby/presence/np_cpp_ffi/shared/shared_test_util.h
@@ -18,7 +18,7 @@
 #include "nearby_protocol.h"
 
 inline nearby_protocol::RawAdvertisementPayload
-    V0AdvEmpty(nearby_protocol::ByteBuffer<255>({1, {0x00}}));
+    V0AdvEmpty(nearby_protocol::ByteBuffer<255>({2, {0x00, 0x03}}));
 
 inline nearby_protocol::RawAdvertisementPayload
     V0AdvSimple(nearby_protocol::ByteBuffer<255>({
@@ -37,20 +37,6 @@
              0x03,      // Public Identity DE header
              0x15, 0x03 // Length 1 Tx Power DE with value 3
          }}));
-
-inline nearby_protocol::RawAdvertisementPayload
-    V1AdvMultipleSections(nearby_protocol::ByteBuffer<255>(
-        {10,
-         {
-             0x20,             // V1 Advertisement header
-             0x04,             // Section Header
-             0x03,             // Public Identity DE header
-             0x26, 0x00, 0x46, // Length 2 Actions DE
-             0x03,             // Section Header
-             0x03,             // Public Identity DE header
-             0x15, 0x03        // Length 1 Tx Power DE with value 3
-         }}));
-
 void test_panic_handler(nearby_protocol::PanicReason reason);
 
 std::string generate_hex_string(size_t length);
diff --git a/nearby/presence/np_cpp_ffi/tests/credential_book_tests.cc b/nearby/presence/np_cpp_ffi/tests/credential_book_tests.cc
index 5eb9ce2..53218ee 100644
--- a/nearby/presence/np_cpp_ffi/tests/credential_book_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/credential_book_tests.cc
@@ -20,7 +20,7 @@
 TEST(NpFfiCredentialBookTests, TestMoveConstructor) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto deserialize_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(deserialize_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -28,7 +28,7 @@
   // still result in success
   nearby_protocol::CredentialBook next_book(std::move(book));
   auto deserialize_result_moved =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty,
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple,
                                                               next_book);
   ASSERT_EQ(deserialize_result_moved.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
@@ -36,21 +36,21 @@
   // The old object should now lead to use after moved assert failure
   ASSERT_DEATH([[maybe_unused]] auto failure =
                    nearby_protocol::Deserializer::DeserializeAdvertisement(
-                       V0AdvEmpty, book), // NOLINT(bugprone-use-after-move)
+                       V0AdvSimple, book), // NOLINT(bugprone-use-after-move)
                "");
 
   // moving again should still lead to a use after moved assert failure
   nearby_protocol::CredentialBook another_moved_book(std::move(book));
   ASSERT_DEATH([[maybe_unused]] auto failure =
                    nearby_protocol::Deserializer::DeserializeAdvertisement(
-                       V0AdvEmpty, another_moved_book),
+                       V0AdvSimple, another_moved_book),
                "");
 }
 
 TEST(NpFfiCredentialBookTests, TestMoveAssignment) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto deserialize_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(deserialize_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -60,7 +60,7 @@
 
   // new credential book should still be successful
   auto deserialize_result_other =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty,
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple,
                                                               other_book);
   ASSERT_EQ(deserialize_result_other.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
@@ -68,13 +68,13 @@
   // The old object should now lead to use after moved assert failure
   ASSERT_DEATH([[maybe_unused]] auto failure =
                    nearby_protocol::Deserializer::DeserializeAdvertisement(
-                       V0AdvEmpty, book), // NOLINT(bugprone-use-after-move)
+                       V0AdvSimple, book), // NOLINT(bugprone-use-after-move)
                "");
 
   // moving again should still lead to a use after moved assert failure
   auto another_moved_book = std::move(book);
   ASSERT_DEATH([[maybe_unused]] auto failure =
                    nearby_protocol::Deserializer::DeserializeAdvertisement(
-                       V0AdvEmpty, another_moved_book),
+                       V0AdvSimple, another_moved_book),
                "");
 }
\ No newline at end of file
diff --git a/nearby/presence/np_cpp_ffi/tests/deserialize_result_tests.cc b/nearby/presence/np_cpp_ffi/tests/deserialize_result_tests.cc
index e3a44d5..54872c6 100644
--- a/nearby/presence/np_cpp_ffi/tests/deserialize_result_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/deserialize_result_tests.cc
@@ -21,7 +21,7 @@
 TEST(NpFfiDeserializeResultTests, TestResultMoveConstructor) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -89,13 +89,13 @@
 TEST(NpFfiDeserializeResultTests, TestResultMoveAssignment) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
   // create a second result
   auto another_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(another_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -165,7 +165,7 @@
   ASSERT_TRUE(maybe_credential_book.ok());
   auto deserialize_result =
       nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V0AdvEmpty, maybe_credential_book.value());
+          V0AdvSimple, maybe_credential_book.value());
 
   ASSERT_EQ(deserialize_result.GetKind(),
             nearby_protocol::DeserializeAdvertisementResultKind::V0);
@@ -179,7 +179,7 @@
 
   auto deserialize_result =
       nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V0AdvEmpty, book_result.value());
+          V0AdvSimple, book_result.value());
   ASSERT_EQ(deserialize_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -215,7 +215,7 @@
 
   auto deserialize_result =
       nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V0AdvEmpty, book_result.value());
+          V0AdvSimple, book_result.value());
   ASSERT_EQ(deserialize_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -253,7 +253,7 @@
 
   auto deserialize_result =
       nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V0AdvEmpty, book_result.value());
+          V0AdvSimple, book_result.value());
   ASSERT_EQ(deserialize_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
diff --git a/nearby/presence/np_cpp_ffi/tests/deserialize_v0_tests.cc b/nearby/presence/np_cpp_ffi/tests/deserialize_v0_tests.cc
index 9781720..cd95c48 100644
--- a/nearby/presence/np_cpp_ffi/tests/deserialize_v0_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/deserialize_v0_tests.cc
@@ -207,29 +207,13 @@
           V0AdvEmpty, maybe_credential_book.value());
 
   ASSERT_EQ(deserialize_result.GetKind(),
-            nearby_protocol::DeserializeAdvertisementResultKind::V0);
-  auto v0_adv = deserialize_result.IntoV0();
-
-  ASSERT_EQ(v0_adv.GetKind(),
-            nearby_protocol::DeserializedV0AdvertisementKind::Legible);
-  auto legible_adv = v0_adv.IntoLegible();
-  auto identity = legible_adv.GetIdentity();
-  ASSERT_EQ(identity.GetKind(),
-            nearby_protocol::DeserializedV0IdentityKind::Plaintext);
-
-  auto num_des = legible_adv.GetNumberOfDataElements();
-  ASSERT_EQ(num_des, 0);
-  auto payload = legible_adv.IntoPayload();
-
-  auto result = payload.TryGetDataElement(0);
-  ASSERT_FALSE(result.ok());
-  ASSERT_TRUE(absl::IsOutOfRange(result.status()));
+            nearby_protocol::DeserializeAdvertisementResultKind::Error);
 }
 
 TEST(NpFfiDeserializeV0Tests, TestV0AdvMoveConstructor) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto adv = result.IntoV0();
@@ -256,14 +240,14 @@
 TEST(NpFfiDeserializeResultTests, TestV0AdvMoveAssignment) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto adv = result.IntoV0();
 
   // create a second result
   auto another_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(another_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto adv2 = another_result.IntoV0();
@@ -293,14 +277,14 @@
   {
     auto deserialize_result =
         nearby_protocol::Deserializer::DeserializeAdvertisement(
-            V0AdvEmpty, book_result.value());
+            V0AdvSimple, book_result.value());
     ASSERT_EQ(deserialize_result.GetKind(),
               np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
     // Going over max amount should result in error
     auto deserialize_result2 =
         nearby_protocol::Deserializer::DeserializeAdvertisement(
-            V0AdvEmpty, book_result.value());
+            V0AdvSimple, book_result.value());
     ASSERT_EQ(deserialize_result2.GetKind(),
               np_ffi::internal::DeserializeAdvertisementResultKind::Error);
 
@@ -313,7 +297,7 @@
   // will create room for one more to be created.
   auto deserialize_result3 =
       nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V0AdvEmpty, book_result.value());
+          V0AdvSimple, book_result.value());
   ASSERT_EQ(deserialize_result3.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 }
@@ -341,14 +325,14 @@
 TEST(NpFfiDeserializeV0Tests, TestLegibleAdvMoveConstructor) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto legible = result.IntoV0().IntoLegible();
 
   // Now move the adv into a new value, and make sure its still valid
   nearby_protocol::LegibleDeserializedV0Advertisement moved(std::move(legible));
-  ASSERT_EQ(moved.GetNumberOfDataElements(), 0);
+  ASSERT_EQ(moved.GetNumberOfDataElements(), 1);
   ASSERT_EQ(moved.GetIdentity().GetKind(),
             np_ffi::internal::DeserializedV0IdentityKind::Plaintext);
 
@@ -375,14 +359,14 @@
 TEST(NpFfiDeserializeResultTests, TestLegibleAdvMoveAssignment) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto legible = result.IntoV0().IntoLegible();
 
   // create a second result
   auto another_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(another_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto legible2 = another_result.IntoV0().IntoLegible();
diff --git a/nearby/presence/np_cpp_ffi/tests/deserialize_v1_tests.cc b/nearby/presence/np_cpp_ffi/tests/deserialize_v1_tests.cc
index 48b881b..115ad14 100644
--- a/nearby/presence/np_cpp_ffi/tests/deserialize_v1_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/deserialize_v1_tests.cc
@@ -55,50 +55,6 @@
   ASSERT_EQ(vec, expected);
 }
 
-TEST(NpFfiDeserializeV1Tests, V1MultipleSections) {
-  auto maybe_credential_book = nearby_protocol::CredentialBook::TryCreate();
-  ASSERT_TRUE(maybe_credential_book.ok());
-
-  auto deserialize_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V1AdvMultipleSections, maybe_credential_book.value());
-  ASSERT_EQ(deserialize_result.GetKind(),
-            nearby_protocol::DeserializeAdvertisementResultKind::V1);
-
-  auto v1_adv = deserialize_result.IntoV1();
-  ASSERT_EQ(v1_adv.GetNumLegibleSections(), 2);
-  ASSERT_EQ(v1_adv.GetNumUndecryptableSections(), 0);
-
-  auto invalid = v1_adv.TryGetSection(2);
-  ASSERT_FALSE(invalid.ok());
-  ASSERT_TRUE(absl::IsOutOfRange(invalid.status()));
-
-  auto section = v1_adv.TryGetSection(0);
-  ASSERT_TRUE(section.ok());
-  ASSERT_EQ(section.value().GetIdentityKind(),
-            nearby_protocol::DeserializedV1IdentityKind::Plaintext);
-  ASSERT_EQ(section.value().NumberOfDataElements(), 1);
-
-  auto invalid_de = section.value().TryGetDataElement(1);
-  ASSERT_FALSE(invalid_de.ok());
-  ASSERT_TRUE(absl::IsOutOfRange(invalid_de.status()));
-
-  auto de = section.value().TryGetDataElement(0);
-  ASSERT_TRUE(de.ok());
-  ASSERT_EQ(de.value().GetDataElementTypeCode(), 6);
-
-  auto payload = de.value().GetPayload();
-  auto vec = payload.ToVector();
-  std::vector<uint8_t> expected{0x00, 0x46};
-  ASSERT_EQ(vec, expected);
-
-  auto section2 = v1_adv.TryGetSection(1);
-  ASSERT_TRUE(section2.ok());
-  ASSERT_EQ(section2.value().GetIdentityKind(),
-            nearby_protocol::DeserializedV1IdentityKind::Plaintext);
-  ASSERT_EQ(section2.value().NumberOfDataElements(), 1);
-}
-
 TEST(NpFfiDeserializeV1Tests, TestV1AdvMoveConstructor) {
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto result = nearby_protocol::Deserializer::DeserializeAdvertisement(
@@ -216,39 +172,50 @@
   ASSERT_TRUE(TryDeserializeNewV1Adv(maybe_credential_book.value()));
 }
 
-void NestedGetSectionsPart2(nearby_protocol::DeserializedV1Advertisement &adv,
-                            nearby_protocol::CredentialBook &book) {
-  { auto section = adv.TryGetSection(1); }
-  assert(!TryDeserializeNewV1Adv(book));
-}
-
-nearby_protocol::DeserializedV1Section
-NestedGetSections(nearby_protocol::CredentialBook &book) {
-  auto v1_adv = nearby_protocol::Deserializer::DeserializeAdvertisement(
-                    V1AdvMultipleSections, book)
-                    .IntoV1();
-  auto section = v1_adv.TryGetSection(0);
-  NestedGetSectionsPart2(v1_adv, book);
-  return section.value();
-}
-
-// Make sure handle stays in scope until all of the sections and adv have gone
-// out of scope
-TEST(NpFfiDeserializeV1Tests, TestSectionOwnershipMultipleSections) {
-  // Capping the max number so we can verify that de-allocations are happening
-  nearby_protocol::GlobalConfig::SetMaxNumDeserializedV1Advertisements(1);
+/*
+ * Multiple sections are not supported in plaintext advertisements
+ * TODO Update the below test to use encrypted sections
+TEST(NpFfiDeserializeV1Tests, V1MultipleSections) {
   auto maybe_credential_book = nearby_protocol::CredentialBook::TryCreate();
   ASSERT_TRUE(maybe_credential_book.ok());
-  {
-    auto section = NestedGetSections(maybe_credential_book.value());
-    ASSERT_EQ(section.GetIdentityKind(),
-              nearby_protocol::DeserializedV1IdentityKind::Plaintext);
-    ASSERT_EQ(section.NumberOfDataElements(), 1);
 
-    ASSERT_FALSE(TryDeserializeNewV1Adv(maybe_credential_book.value()));
-  }
+  auto deserialize_result =
+      nearby_protocol::Deserializer::DeserializeAdvertisement(
+          V1AdvMultipleSections, maybe_credential_book.value());
+  ASSERT_EQ(deserialize_result.GetKind(),
+            nearby_protocol::DeserializeAdvertisementResultKind::V1);
 
-  // now that the section has gone out of scope, deserializing a new adv should
-  // succeed
-  ASSERT_TRUE(TryDeserializeNewV1Adv(maybe_credential_book.value()));
-}
\ No newline at end of file
+  auto v1_adv = deserialize_result.IntoV1();
+  ASSERT_EQ(v1_adv.GetNumLegibleSections(), 2);
+  ASSERT_EQ(v1_adv.GetNumUndecryptableSections(), 0);
+
+  auto invalid = v1_adv.TryGetSection(2);
+  ASSERT_FALSE(invalid.ok());
+  ASSERT_TRUE(absl::IsOutOfRange(invalid.status()));
+
+  auto section = v1_adv.TryGetSection(0);
+  ASSERT_TRUE(section.ok());
+  ASSERT_EQ(section.value().GetIdentityKind(),
+            nearby_protocol::DeserializedV1IdentityKind::Plaintext);
+  ASSERT_EQ(section.value().NumberOfDataElements(), 1);
+
+  auto invalid_de = section.value().TryGetDataElement(1);
+  ASSERT_FALSE(invalid_de.ok());
+  ASSERT_TRUE(absl::IsOutOfRange(invalid_de.status()));
+
+  auto de = section.value().TryGetDataElement(0);
+  ASSERT_TRUE(de.ok());
+  ASSERT_EQ(de.value().GetDataElementTypeCode(), 6);
+
+  auto payload = de.value().GetPayload();
+  auto vec = payload.ToVector();
+  std::vector<uint8_t> expected{0x00, 0x46};
+  ASSERT_EQ(vec, expected);
+
+  auto section2 = v1_adv.TryGetSection(1);
+  ASSERT_TRUE(section2.ok());
+  ASSERT_EQ(section2.value().GetIdentityKind(),
+            nearby_protocol::DeserializedV1IdentityKind::Plaintext);
+  ASSERT_EQ(section2.value().NumberOfDataElements(), 1);
+}
+*/
\ No newline at end of file
diff --git a/nearby/presence/np_cpp_ffi/tests/global_config_tests.cc b/nearby/presence/np_cpp_ffi/tests/global_config_tests.cc
index e120aee..6936f23 100644
--- a/nearby/presence/np_cpp_ffi/tests/global_config_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/global_config_tests.cc
@@ -24,7 +24,7 @@
       nearby_protocol::GlobalConfig::SetPanicHandler(test_panic_handler));
   auto book = nearby_protocol::CredentialBook::TryCreate().value();
   auto deserialize_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(deserialize_result.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
@@ -54,7 +54,7 @@
   auto book2 = nearby_protocol::CredentialBook::TryCreate().value();
   auto book3 = nearby_protocol::CredentialBook::TryCreate().value();
   auto deserialize_result =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
 
   // Should still work
   ASSERT_EQ(deserialize_result.GetKind(),
@@ -64,12 +64,12 @@
   // still work.
   nearby_protocol::GlobalConfig::SetNumShards(1);
   auto deserialize_result_2 =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty,
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple,
                                                               book2);
   ASSERT_EQ(deserialize_result_2.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto deserialize_result_3 =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty,
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple,
                                                               book3);
   ASSERT_EQ(deserialize_result_3.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
@@ -81,11 +81,11 @@
 
   // should still be able to parse 2 payloads with only one shard
   auto deserialize_result1 =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(deserialize_result1.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
   auto deserialize_result2 =
-      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvEmpty, book);
+      nearby_protocol::Deserializer::DeserializeAdvertisement(V0AdvSimple, book);
   ASSERT_EQ(deserialize_result2.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 }
@@ -119,14 +119,14 @@
   {
     auto deserialize_result =
         nearby_protocol::Deserializer::DeserializeAdvertisement(
-            V0AdvEmpty, book_result.value());
+            V0AdvSimple, book_result.value());
     ASSERT_EQ(deserialize_result.GetKind(),
               np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 
     // Going over max amount should result in error
     auto deserialize_result2 =
         nearby_protocol::Deserializer::DeserializeAdvertisement(
-            V0AdvEmpty, book_result.value());
+            V0AdvSimple, book_result.value());
     ASSERT_EQ(deserialize_result2.GetKind(),
               np_ffi::internal::DeserializeAdvertisementResultKind::Error);
   }
@@ -135,7 +135,7 @@
   // will create room for one more to be created.
   auto deserialize_result3 =
       nearby_protocol::Deserializer::DeserializeAdvertisement(
-          V0AdvEmpty, book_result.value());
+          V0AdvSimple, book_result.value());
   ASSERT_EQ(deserialize_result3.GetKind(),
             np_ffi::internal::DeserializeAdvertisementResultKind::V0);
 }
diff --git a/nearby/presence/np_ed25519/Cargo.toml b/nearby/presence/np_ed25519/Cargo.toml
index b4bca66..601fc82 100644
--- a/nearby/presence/np_ed25519/Cargo.toml
+++ b/nearby/presence/np_ed25519/Cargo.toml
@@ -6,7 +6,7 @@
 
 [dependencies]
 array_view.workspace = true
-crypto_provider.workspace = true
+crypto_provider = {workspace = true, features = ["raw_private_key_permit"]}
 sink.workspace = true
 tinyvec.workspace = true
 
diff --git a/nearby/presence/np_ed25519/src/lib.rs b/nearby/presence/np_ed25519/src/lib.rs
index 60c8302..81a1a10 100644
--- a/nearby/presence/np_ed25519/src/lib.rs
+++ b/nearby/presence/np_ed25519/src/lib.rs
@@ -26,8 +26,8 @@
 
 use array_view::ArrayView;
 use crypto_provider::ed25519::{
-    Ed25519Provider, KeyPair as _, PublicKey as _, RawPrivateKey, RawPublicKey, RawSignature,
-    Signature as _, SignatureError,
+    Ed25519Provider, KeyPair as _, PrivateKey, PublicKey as _, RawPrivateKey, RawPrivateKeyPermit,
+    RawPublicKey, RawSignature, Signature as _, SignatureError,
 };
 use crypto_provider::CryptoProvider;
 use sink::{Sink, SinkWriter};
@@ -55,15 +55,30 @@
 pub struct KeyPair<C: CryptoProvider>(CpKeyPair<C>);
 
 impl<C: CryptoProvider> KeyPair<C> {
-    /// Returns the `KeyPair`'s private key bytes. This method should only ever be called by code
-    /// which securely stores private credentials.
-    pub fn private_key(&self) -> RawPrivateKey {
+    /// Returns the `KeyPair`'s private key bytes.
+    /// This method is only usable in situations where
+    /// the caller has permission to handle the raw bytes
+    /// of a private key.
+    pub fn raw_private_key(&self, permit: &RawPrivateKeyPermit) -> RawPrivateKey {
+        self.0.raw_private_key(permit)
+    }
+
+    /// Builds this key-pair from an array of its private key bytes in the format
+    /// yielded by `private_key`.
+    /// This method is only usable in situations where
+    /// the caller has permission to handle the raw bytes
+    /// of a private key.
+    pub fn from_raw_private_key(private_key: &RawPrivateKey, permit: &RawPrivateKeyPermit) -> Self {
+        Self(CpKeyPair::<C>::from_raw_private_key(private_key, permit))
+    }
+
+    /// Returns the private key of this key-pair.
+    pub fn private_key(&self) -> PrivateKey {
         self.0.private_key()
     }
 
-    /// Builds this key-pair from an array of its private key bytes in the yielded by `private_key`.
-    /// This method should only ever be called by code which securely stores private credentials.
-    pub fn from_private_key(private_key: &RawPrivateKey) -> Self {
+    /// Builds this key-pair from a private key.
+    pub fn from_private_key(private_key: &PrivateKey) -> Self {
         Self(CpKeyPair::<C>::from_private_key(private_key))
     }
 
@@ -127,6 +142,11 @@
 }
 
 impl<C: CryptoProvider> PublicKey<C> {
+    /// Constructs a public key for NP adv signature verification
+    /// from a public key under the given crypto-provider
+    pub fn new(public_key: CpPublicKey<C>) -> Self {
+        Self { public_key }
+    }
     /// Succeeds if the signature was a valid signature created via the corresponding
     /// keypair to this public key using the given [`SignatureContext`] on the given
     /// message payload. The message payload is represented
diff --git a/nearby/presence/np_ffi_core/Cargo.toml b/nearby/presence/np_ffi_core/Cargo.toml
index 5be03a6..cdcc7d3 100644
--- a/nearby/presence/np_ffi_core/Cargo.toml
+++ b/nearby/presence/np_ffi_core/Cargo.toml
@@ -9,9 +9,12 @@
 np_adv.workspace = true
 handle_map.workspace = true
 crypto_provider.workspace = true
-#TODO: Allow this to be configurable!
-crypto_provider_default = {workspace = true, features = ["rustcrypto"]}
-spin.workspace = true
+crypto_provider_default = {workspace = true, default-features = false}
+lock_adapter.workspace = true
+lazy_static.workspace = true
 
 [features]
-std = []
+default = ["rustcrypto"]
+rustcrypto = ["crypto_provider_default/rustcrypto", "crypto_provider_default/std"]
+opensslbssl = ["crypto_provider_default/opensslbssl"]
+boringssl = ["crypto_provider_default/boringssl"]
diff --git a/nearby/presence/np_ffi_core/src/common.rs b/nearby/presence/np_ffi_core/src/common.rs
index f00c358..1683b68 100644
--- a/nearby/presence/np_ffi_core/src/common.rs
+++ b/nearby/presence/np_ffi_core/src/common.rs
@@ -14,9 +14,11 @@
 //! Common externally-accessible FFI constructs which are needed
 //! in order to define the interfaces in this crate's various modules.
 
-use alloc::string::String;
 use array_view::ArrayView;
 use handle_map::HandleNotPresentError;
+use lock_adapter::std::RwLock;
+use lock_adapter::RwLock as _;
+use std::string::String;
 
 const DEFAULT_MAX_HANDLES: u32 = u32::MAX - 1;
 
@@ -133,7 +135,7 @@
     }
 }
 
-static COMMON_CONFIG: spin::RwLock<CommonConfig> = spin::RwLock::new(CommonConfig::new());
+static COMMON_CONFIG: RwLock<CommonConfig> = RwLock::new(CommonConfig::new());
 
 pub(crate) fn global_num_shards() -> u8 {
     COMMON_CONFIG.read().num_shards()
diff --git a/nearby/presence/np_ffi_core/src/credentials.rs b/nearby/presence/np_ffi_core/src/credentials.rs
index 412ebc1..5392417 100644
--- a/nearby/presence/np_ffi_core/src/credentials.rs
+++ b/nearby/presence/np_ffi_core/src/credentials.rs
@@ -19,7 +19,7 @@
 
 /// Internal, Rust-side implementation of a credential-book.
 /// See [`CredentialBook`] for the FFI-side handles.
-// TODO(b/283249542): Give this a real definition!
+// TODO: Give this a real definition!
 pub struct CredentialBookInternals;
 
 fn get_credential_book_handle_map_dimensions() -> HandleMapDimensions {
@@ -29,8 +29,13 @@
     }
 }
 
-declare_handle_map! {credential_book, CredentialBook, super::CredentialBookInternals, super::get_credential_book_handle_map_dimensions()}
-use credential_book::CredentialBook;
+declare_handle_map! {
+    mod credential_book {
+        #[dimensions = super::get_credential_book_handle_map_dimensions()]
+        type CredentialBook: HandleLike<Object = super::CredentialBookInternals>;
+    }
+}
+pub use credential_book::CredentialBook;
 
 /// Discriminant for `CreateCredentialBookResult`
 #[repr(u8)]
diff --git a/nearby/presence/np_ffi_core/src/deserialize/mod.rs b/nearby/presence/np_ffi_core/src/deserialize/mod.rs
index a651bcf..9c20fde 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/mod.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/mod.rs
@@ -20,6 +20,7 @@
 use crate::utils::FfiEnum;
 use crypto_provider_default::CryptoProviderImpl;
 use handle_map::{HandleLike, HandleMapFullError, HandleNotPresentError};
+use np_adv::deserialization_arena;
 
 pub mod v0;
 pub mod v1;
@@ -106,17 +107,6 @@
     }
 }
 
-type DefaultV0Credential = np_adv::credential::simple::SimpleV0Credential<
-    np_adv::credential::v0::MinimumFootprintV0CryptoMaterial,
-    (),
->;
-type DefaultV1Credential = np_adv::credential::simple::SimpleV1Credential<
-    np_adv::credential::v1::MinimumFootprintV1CryptoMaterial,
-    (),
->;
-type DefaultBothCredentialSource =
-    np_adv::credential::source::OwnedBothCredentialSource<DefaultV0Credential, DefaultV1Credential>;
-
 fn deserialize_advertisement_from_slice_internal(
     adv_payload: &[u8],
     credential_book: CredentialBook,
@@ -124,14 +114,14 @@
     // Deadlock Safety: Credential-book locks always live longer than deserialized advs.
     let _credential_book_read_guard = credential_book.get()?;
 
-    //TODO(b/283249542): Use an actual credential source
-    let cred_source: DefaultBothCredentialSource = DefaultBothCredentialSource::new_empty();
+    //TODO: Use an actual credential source
+    let cred_book = np_adv::credential::book::CredentialBookBuilder::<
+        np_adv::credential::EmptyMatchedCredential,
+    >::build_cached_slice_book::<0, 0, CryptoProviderImpl>(&[], &[]);
 
+    let arena = deserialization_arena!();
     let deserialized_advertisement =
-        np_adv::deserialize_advertisement::<_, _, _, _, CryptoProviderImpl>(
-            adv_payload,
-            &cred_source,
-        )?;
+        np_adv::deserialize_advertisement::<_, CryptoProviderImpl>(arena, adv_payload, &cred_book)?;
     match deserialized_advertisement {
         np_adv::DeserializedAdvertisement::V0(adv_contents) => {
             let adv_handle = DeserializedV0Advertisement::allocate_with_contents(adv_contents)?;
diff --git a/nearby/presence/np_ffi_core/src/deserialize/v0.rs b/nearby/presence/np_ffi_core/src/deserialize/v0.rs
index 0f4b24b..7b9ee75 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/v0.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/v0.rs
@@ -16,10 +16,10 @@
 use crate::common::*;
 use crate::credentials::credential_book::CredentialBook;
 use crate::utils::{FfiEnum, LocksLongerThan};
-use alloc::vec::Vec;
 use handle_map::{declare_handle_map, HandleLike, HandleMapDimensions, HandleMapFullError};
 use np_adv::legacy::actions::ActionsDataElement;
 use np_adv::legacy::{data_elements as np_adv_de, Ciphertext, PacketFlavorEnum, Plaintext};
+use std::vec::Vec;
 
 /// Discriminant for possible results of V0 advertisement deserialization
 #[derive(Clone, Copy)]
@@ -66,20 +66,22 @@
         }
     }
 
-    pub(crate) fn allocate_with_contents<'m, M: np_adv::credential::MatchedCredential<'m>>(
-        contents: np_adv::V0AdvContents<'m, M>,
+    pub(crate) fn allocate_with_contents<M: np_adv::credential::MatchedCredential>(
+        contents: np_adv::V0AdvertisementContents<M>,
     ) -> Result<Self, HandleMapFullError> {
         match contents {
-            np_adv::V0AdvContents::Plaintext(plaintext_contents) => {
+            np_adv::V0AdvertisementContents::Plaintext(plaintext_contents) => {
                 let adv = LegibleDeserializedV0Advertisement::allocate_with_plaintext_contents(
                     plaintext_contents,
                 )?;
                 Ok(Self::Legible(adv))
             }
-            np_adv::V0AdvContents::Decrypted(_) => {
+            np_adv::V0AdvertisementContents::Decrypted(_) => {
                 unimplemented!();
             }
-            np_adv::V0AdvContents::NoMatchingCredentials => Ok(Self::NoMatchingCredentials),
+            np_adv::V0AdvertisementContents::NoMatchingCredentials => {
+                Ok(Self::NoMatchingCredentials)
+            }
         }
     }
 
@@ -109,11 +111,11 @@
         self.num_des
     }
     /// Destructures this legible advertisement into just the payload
-    pub fn into_payload(self) -> V0Payload {
+    pub fn payload(&self) -> V0Payload {
         self.payload
     }
     /// Destructures this legible advertisement into just the identity information
-    pub fn into_identity(self) -> DeserializedV0Identity {
+    pub fn identity(&self) -> DeserializedV0Identity {
         self.identity
     }
     /// Deallocates the underlying handle of the payload
@@ -136,6 +138,7 @@
 /// by a deserialized V0 advertisement
 #[repr(C)]
 #[allow(missing_docs)]
+#[derive(Copy, Clone, Eq, PartialEq)]
 pub enum DeserializedV0Identity {
     Plaintext,
     // TODO: This gets a payload once we support creds
@@ -176,7 +179,12 @@
     }
 }
 
-declare_handle_map! {v0_payload, V0Payload, super::V0PayloadInternals, super::get_v0_payload_handle_map_dimensions() }
+declare_handle_map! {
+    mod v0_payload {
+        #[dimensions = super::get_v0_payload_handle_map_dimensions()]
+        type V0Payload: HandleLike<Object = super::V0PayloadInternals>;
+    }
+}
 use v0_payload::V0Payload;
 
 impl LocksLongerThan<V0Payload> for CredentialBook {}
@@ -327,10 +335,10 @@
     fn from(value: ActionsDataElement<F>) -> Self {
         match F::ENUM_VARIANT {
             PacketFlavorEnum::Plaintext => {
-                Self::Plaintext(V0ActionBits { bitfield: value.as_u32() })
+                Self::Plaintext(V0ActionBits { bitfield: value.action.as_u32() })
             }
             PacketFlavorEnum::Ciphertext => {
-                Self::Encrypted(V0ActionBits { bitfield: value.as_u32() })
+                Self::Encrypted(V0ActionBits { bitfield: value.action.as_u32() })
             }
         }
     }
@@ -402,6 +410,7 @@
 
                 let actions_de = np_adv::legacy::actions::ActionsDataElement::from(bits);
                 Ok(actions_de
+                    .action
                     .has_action(&action_type.into())
                     .expect("BooleanActionType only has one bit"))
             }
@@ -413,6 +422,7 @@
 
                 let actions_de = np_adv::legacy::actions::ActionsDataElement::from(bits);
                 Ok(actions_de
+                    .action
                     .has_action(&action_type.into())
                     .expect("BooleanActionType only has one bit"))
             }
@@ -430,7 +440,7 @@
                 .map_err(|_| InvalidActionBits)?;
 
                 let actions_de = np_adv::legacy::actions::ActionsDataElement::from(bits);
-                Ok(actions_de.context_sync_seq_num().as_u8())
+                Ok(actions_de.action.context_sync_seq_num().as_u8())
             }
             V0Actions::Encrypted(action_bits) => {
                 let bits = np_adv::legacy::actions::ActionBits::<Ciphertext>::try_from(
@@ -438,7 +448,7 @@
                 )
                 .map_err(|_| InvalidActionBits)?;
                 let actions_de = np_adv::legacy::actions::ActionsDataElement::from(bits);
-                Ok(actions_de.context_sync_seq_num().as_u8())
+                Ok(actions_de.action.context_sync_seq_num().as_u8())
             }
         }
     }
diff --git a/nearby/presence/np_ffi_core/src/deserialize/v1.rs b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
index 9b4a3f4..be44fb7 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/v1.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
@@ -16,9 +16,10 @@
 use crate::common::*;
 use crate::credentials::credential_book::CredentialBook;
 use crate::utils::*;
-use alloc::vec::Vec;
 use array_view::ArrayView;
 use handle_map::{declare_handle_map, HandleLike, HandleMapDimensions, HandleMapFullError};
+use np_adv::extended::deserialize::DataElementParseError;
+use std::vec::Vec;
 
 /// Representation of a deserialized V1 advertisement
 #[repr(C)]
@@ -56,12 +57,12 @@
         self.legible_sections.deallocate().map(|_| ()).into()
     }
 
-    pub(crate) fn allocate_with_contents<'m, M: np_adv::credential::MatchedCredential<'m>>(
-        contents: np_adv::V1AdvContents<'m, M>,
+    pub(crate) fn allocate_with_contents<M: np_adv::credential::MatchedCredential>(
+        contents: np_adv::V1AdvertisementContents<M>,
     ) -> Result<Self, HandleMapFullError> {
         // 16-section limit enforced by np_adv
         let num_undecryptable_sections = contents.invalid_sections_count() as u8;
-        let legible_sections = contents.into_valid_sections();
+        let legible_sections = contents.into_sections();
         let num_legible_sections = legible_sections.len() as u8;
         let legible_sections = LegibleV1Sections::allocate_with_contents(legible_sections)?;
         Ok(Self { num_undecryptable_sections, num_legible_sections, legible_sections })
@@ -105,11 +106,14 @@
     }
 }
 
-impl<'m, M: np_adv::credential::MatchedCredential<'m>>
-    From<Vec<np_adv::V1DeserializedSection<'m, M>>> for LegibleV1SectionsInternals
+impl<'adv, M: np_adv::credential::MatchedCredential>
+    From<Vec<np_adv::V1DeserializedSection<'adv, M>>> for LegibleV1SectionsInternals
 {
-    fn from(contents: Vec<np_adv::V1DeserializedSection<'m, M>>) -> Self {
-        let sections = contents.into_iter().map(DeserializedV1SectionInternals::from).collect();
+    fn from(contents: Vec<np_adv::V1DeserializedSection<'adv, M>>) -> Self {
+        let sections = contents
+            .into_iter()
+            .filter_map(|s| DeserializedV1SectionInternals::try_from(s).ok())
+            .collect();
         Self { sections }
     }
 }
@@ -121,14 +125,19 @@
     }
 }
 
-declare_handle_map! {legible_v1_sections, LegibleV1Sections, super::LegibleV1SectionsInternals, super::get_legible_v1_sections_handle_map_dimensions() }
+declare_handle_map! {
+    mod legible_v1_sections {
+        #[dimensions = super::get_legible_v1_sections_handle_map_dimensions()]
+        type LegibleV1Sections: HandleLike<Object = super::LegibleV1SectionsInternals>;
+    }
+}
 use legible_v1_sections::LegibleV1Sections;
 
 impl LocksLongerThan<LegibleV1Sections> for CredentialBook {}
 
 impl LegibleV1Sections {
-    pub(crate) fn allocate_with_contents<'m, M: np_adv::credential::MatchedCredential<'m>>(
-        contents: Vec<np_adv::V1DeserializedSection<'m, M>>,
+    pub(crate) fn allocate_with_contents<M: np_adv::credential::MatchedCredential>(
+        contents: Vec<np_adv::V1DeserializedSection<M>>,
     ) -> Result<Self, HandleMapFullError> {
         Self::allocate(move || LegibleV1SectionsInternals::from(contents))
     }
@@ -197,17 +206,22 @@
     }
 }
 
-impl<'m, M: np_adv::credential::MatchedCredential<'m>> From<np_adv::V1DeserializedSection<'m, M>>
+impl<'adv, M: np_adv::credential::MatchedCredential> TryFrom<np_adv::V1DeserializedSection<'adv, M>>
     for DeserializedV1SectionInternals
 {
-    fn from(section: np_adv::V1DeserializedSection<'m, M>) -> Self {
+    type Error = DataElementParseError;
+
+    fn try_from(section: np_adv::V1DeserializedSection<M>) -> Result<Self, Self::Error> {
         use np_adv::extended::deserialize::Section;
         use np_adv::V1DeserializedSection;
         match section {
             V1DeserializedSection::Plaintext(section) => {
-                let des = section.data_elements().map(V1DataElement::from).collect();
+                let des = section
+                    .iter_data_elements()
+                    .map(|r| r.map(|de| V1DataElement::from(&de)))
+                    .collect::<Result<Vec<_>, _>>()?;
                 let identity = DeserializedV1Identity::Plaintext;
-                Self { des, identity }
+                Ok(Self { des, identity })
             }
             V1DeserializedSection::Decrypted(_) => {
                 unimplemented!();
@@ -341,8 +355,8 @@
     }
 }
 
-impl<'a> From<np_adv::extended::deserialize::DataElement<'a>> for V1DataElement {
-    fn from(de: np_adv::extended::deserialize::DataElement<'a>) -> Self {
+impl<'a> From<&'a np_adv::extended::deserialize::DataElement<'a>> for V1DataElement {
+    fn from(de: &'a np_adv::extended::deserialize::DataElement<'a>) -> Self {
         let de_type = V1DEType::from(de.de_type());
         let contents_as_slice = de.contents();
         //Guaranteed not to panic due DE size limit.
diff --git a/nearby/presence/np_ffi_core/src/lib.rs b/nearby/presence/np_ffi_core/src/lib.rs
index dd9e49e..bba33e2 100644
--- a/nearby/presence/np_ffi_core/src/lib.rs
+++ b/nearby/presence/np_ffi_core/src/lib.rs
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //! Core functionality common to all NP Rust FFI layers
-#![cfg_attr(not(feature = "std"), no_std)]
 #![forbid(unsafe_code)]
 #![deny(
     missing_docs,
@@ -22,8 +21,8 @@
     clippy::expect_used
 )]
 
-extern crate alloc;
-extern crate core;
+#[macro_use]
+extern crate lazy_static;
 
 #[macro_use]
 pub mod utils;
diff --git a/nearby/presence/np_hkdf/src/v1_salt.rs b/nearby/presence/np_hkdf/src/v1_salt.rs
index b139e84..ae327db 100644
--- a/nearby/presence/np_hkdf/src/v1_salt.rs
+++ b/nearby/presence/np_hkdf/src/v1_salt.rs
@@ -60,6 +60,11 @@
         self.data.as_slice()
     }
 
+    /// Returns the salt bytes as an array
+    pub fn into_array(self) -> [u8; 16] {
+        self.data
+    }
+
     /// Returns the salt bytes as a reference to an array
     pub fn as_array_ref(&self) -> &[u8; 16] {
         &self.data
@@ -91,7 +96,7 @@
 #[derive(PartialEq, Eq, Debug, Clone, Copy, PartialOrd, Ord)]
 pub struct DataElementOffset {
     /// 0-based offset of the DE in the advertisement
-    offset: usize,
+    offset: u8,
 }
 
 impl DataElementOffset {
@@ -99,7 +104,7 @@
     pub const ZERO: DataElementOffset = Self { offset: 0 };
 
     /// Returns the offset as a usize
-    pub fn as_usize(&self) -> usize {
+    pub fn as_u8(&self) -> u8 {
         self.offset
     }
 
@@ -111,8 +116,8 @@
     }
 }
 
-impl From<usize> for DataElementOffset {
-    fn from(num: usize) -> Self {
+impl From<u8> for DataElementOffset {
+    fn from(num: u8) -> Self {
         Self { offset: num }
     }
 }
diff --git a/nearby/presence/xts_aes/Cargo.toml b/nearby/presence/xts_aes/Cargo.toml
index d0b88b6..ba7aadb 100644
--- a/nearby/presence/xts_aes/Cargo.toml
+++ b/nearby/presence/xts_aes/Cargo.toml
@@ -14,9 +14,10 @@
 ldt_tbc.workspace = true
 
 [dev-dependencies]
-crypto_provider_default.workspace = true
+crypto_provider_default = {workspace = true, features = ["rustcrypto"]}
 rand_ext.workspace = true
 test_helper.workspace = true
+wycheproof.workspace = true
 
 anyhow.workspace = true
 base64.workspace = true
diff --git a/nearby/presence/xts_aes/fuzz/Cargo.lock b/nearby/presence/xts_aes/fuzz/Cargo.lock
index f0b7f56..1844bd3 100644
--- a/nearby/presence/xts_aes/fuzz/Cargo.lock
+++ b/nearby/presence/xts_aes/fuzz/Cargo.lock
@@ -25,6 +25,20 @@
 ]
 
 [[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 = "aes-gcm-siv"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -157,6 +171,9 @@
 [[package]]
 name = "crypto_provider"
 version = "0.1.0"
+dependencies = [
+ "tinyvec",
+]
 
 [[package]]
 name = "crypto_provider_rustcrypto"
@@ -164,6 +181,7 @@
 dependencies = [
  "aead",
  "aes",
+ "aes-gcm",
  "aes-gcm-siv",
  "cbc",
  "cfg-if",
@@ -193,9 +211,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.0.0-rc.3"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7"
+checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -261,9 +279,9 @@
 
 [[package]]
 name = "ed25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c"
+checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
 dependencies = [
  "curve25519-dalek",
  "ed25519",
@@ -329,6 +347,16 @@
 ]
 
 [[package]]
+name = "ghash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+dependencies = [
+ "opaque-debug",
+ "polyval",
+]
+
+[[package]]
 name = "group"
 version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -564,6 +592,12 @@
 ]
 
 [[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+
+[[package]]
 name = "typenum"
 version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -599,9 +633,9 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0-rc.3"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/nearby/presence/xts_aes/src/lib.rs b/nearby/presence/xts_aes/src/lib.rs
index ff68d76..ac2cf15 100644
--- a/nearby/presence/xts_aes/src/lib.rs
+++ b/nearby/presence/xts_aes/src/lib.rs
@@ -34,7 +34,7 @@
 use core::fmt;
 use core::marker::PhantomData;
 
-use crypto_provider::aes::{Aes, AesCipher, AesDecryptCipher, AesEncryptCipher};
+use crypto_provider::aes::{Aes, AesBlock, AesCipher, AesDecryptCipher, AesEncryptCipher};
 use crypto_provider::{
     aes::{AesKey, BLOCK_SIZE},
     CryptoProvider,
@@ -58,7 +58,7 @@
 /// little endian bytes (e.g. with `u128::to_le_bytes()`).
 ///
 /// Inside each data unit, there are one or more 16-byte blocks. The length of a data unit SHOULD
-/// not to exceed 2^20 blocks (16GiB) in order to maintain security properties; see
+/// not to exceed 2^20 blocks (16MB) in order to maintain security properties; see
 /// [appendix D](https://luca-giuzzi.unibs.it/corsi/Support/papers-cryptography/1619-2007-NIST-Submission.pdf).
 /// The last block (if there is more than one block) may have fewer than 16 bytes, in which case
 /// ciphertext stealing will be applied.
@@ -104,7 +104,6 @@
 }
 
 #[allow(clippy::expect_used)]
-#[allow(clippy::indexing_slicing)]
 impl<A: Aes<Key = K::BlockCipherKey>, K: XtsKey> XtsEncrypter<A, K> {
     /// Encrypt a data unit in place, using sequential block numbers for each block.
     /// `data_unit` must be at least [BLOCK_SIZE] bytes, and fewer than
@@ -118,20 +117,24 @@
         standalone_blocks.chunks_mut(BLOCK_SIZE).for_each(|block| {
             // Until array_chunks is stabilized, using a macro to get array refs out of slice chunks
             // Won't panic because we are only processing complete blocks
+            #[allow(clippy::indexing_slicing)]
             tweaked_xts.encrypt_block(array_mut_ref!(block, 0, BLOCK_SIZE));
             tweaked_xts.advance_to_next_block_num();
         });
 
         if partial_last_block.is_empty() {
+            #[allow(clippy::indexing_slicing)]
             tweaked_xts.encrypt_block(array_mut_ref!(last_complete_block, 0, BLOCK_SIZE));
             // nothing to do for the last block since it's empty
         } else {
+            // This implements ciphertext stealing for the last two blocks which allows processing
+            // of a message length not divisible by block size without any expansion or padding.
             // b is in bytes not bits; we do not consider partial bytes
             let b = partial_last_block.len();
 
             // Per spec: CC = encrypt P_{m-1} (the last complete block)
             let cc = {
-                let mut last_complete_block_plaintext: crypto_provider::aes::AesBlock =
+                let mut last_complete_block_plaintext: AesBlock =
                     last_complete_block.try_into().expect("complete block");
                 tweaked_xts.encrypt_block(&mut last_complete_block_plaintext);
                 tweaked_xts.advance_to_next_block_num();
@@ -139,7 +142,7 @@
             };
 
             // Copy b bytes of P_m before we overwrite them with C_m
-            let mut partial_last_block_plaintext: crypto_provider::aes::AesBlock = [0; BLOCK_SIZE];
+            let mut partial_last_block_plaintext: AesBlock = [0; BLOCK_SIZE];
             partial_last_block_plaintext
                 .get_mut(0..b)
                 .expect("this should never be hit, since a partial block is always smaller than a complete block")
@@ -148,7 +151,7 @@
             // C_m = first b bytes of CC
             partial_last_block.copy_from_slice(
                 cc.get(0..b)
-                    .expect("partial block len should always be less than a complete block"),
+                    .expect("b is the len of partial_last_block so it will always be in bounds"),
             );
 
             // PP = P_m || last (16 - b) bytes of CC
@@ -173,7 +176,7 @@
         Ok(())
     }
 
-    /// Returns an [XtsTweaked] configured with the specified tweak and a block number of 0.
+    /// Returns an [`XtsEncrypterTweaked`] configured with the specified tweak and a block number of 0.
     fn tweaked(&self, tweak: Tweak) -> XtsEncrypterTweaked<A> {
         let mut bytes = tweak.bytes;
         self.tweak_encryption_cipher.encrypt(&mut bytes);
@@ -186,7 +189,6 @@
 }
 
 #[allow(clippy::expect_used)]
-#[allow(clippy::indexing_slicing)]
 impl<A: Aes<Key = K::BlockCipherKey>, K: XtsKey> XtsDecrypter<A, K> {
     /// Decrypt a data unit in place, using sequential block numbers for each block.
     /// `data_unit` must be at least [BLOCK_SIZE] bytes, and fewer than
@@ -198,11 +200,13 @@
         let mut tweaked_xts = self.tweaked(tweak);
 
         standalone_blocks.chunks_mut(BLOCK_SIZE).for_each(|block| {
+            #[allow(clippy::indexing_slicing)]
             tweaked_xts.decrypt_block(array_mut_ref!(block, 0, BLOCK_SIZE));
             tweaked_xts.advance_to_next_block_num();
         });
 
         if partial_last_block.is_empty() {
+            #[allow(clippy::indexing_slicing)]
             tweaked_xts.decrypt_block(array_mut_ref!(last_complete_block, 0, BLOCK_SIZE));
         } else {
             let b = partial_last_block.len();
@@ -215,7 +219,7 @@
                 tweaked_xts.advance_to_next_block_num();
                 // Block num is now at m
                 // We need C_m later, so make a copy to avoid overwriting
-                let mut last_complete_block_ciphertext: crypto_provider::aes::AesBlock =
+                let mut last_complete_block_ciphertext: AesBlock =
                     last_complete_block.try_into().expect("complete block");
                 tweaked_xts.decrypt_block(&mut last_complete_block_ciphertext);
                 tweaked_xts.set_tweak(tweak_state_m_1);
@@ -223,28 +227,47 @@
             };
 
             // Copy b bytes of C_m before we overwrite them with P_m
-            let mut partial_last_block_ciphertext: crypto_provider::aes::AesBlock = [0; BLOCK_SIZE];
-            partial_last_block_ciphertext[0..b].copy_from_slice(partial_last_block);
+            let mut partial_last_block_ciphertext: AesBlock = [0; BLOCK_SIZE];
+            partial_last_block_ciphertext
+                .get_mut(0..b)
+                .expect("this should never be hit, since a partial block is always smaller than a complete block")
+                .copy_from_slice(partial_last_block);
 
             // P_m = first b bytes of PP
-            partial_last_block.copy_from_slice(&pp[0..b]);
+            partial_last_block.copy_from_slice(
+                pp.get(0..b)
+                    .expect("b is the len of partial_last_block so it will always be in bounds"),
+            );
 
             // CC = C_m | CP (last 16-b bytes of PP)
             let cc = {
-                let cp = &pp[b..];
-                last_complete_block[0..b].copy_from_slice(&partial_last_block_ciphertext[0..b]);
-                last_complete_block[b..].copy_from_slice(cp);
+                let cp = pp.get(b..).expect(
+                    "b is in bounds since a partial block is always smaller than a complete block",
+                );
+                last_complete_block
+                    .get_mut(0..b)
+                    .expect("partial block length within bounds of complete block")
+                    .copy_from_slice(
+                        partial_last_block_ciphertext
+                            .get(0..b)
+                            .expect("partial block length within bounds of complete block"),
+                    );
+                last_complete_block
+                    .get_mut(b..)
+                    .expect("partial block length within bounds of complete block")
+                    .copy_from_slice(cp);
                 last_complete_block
             };
 
             // decrypting at block num = m -1
+            #[allow(clippy::indexing_slicing)]
             tweaked_xts.decrypt_block(array_mut_ref!(cc, 0, BLOCK_SIZE));
         }
 
         Ok(())
     }
 
-    /// Returns an [XtsTweaked] configured with the specified tweak and a block number of 0.
+    /// Returns an [`XtsDecrypterTweaked`] configured with the specified tweak and a block number of 0.
     fn tweaked(&self, tweak: Tweak) -> XtsDecrypterTweaked<A> {
         let mut bytes = tweak.bytes;
         self.tweak_encryption_cipher.encrypt(&mut bytes);
@@ -289,10 +312,10 @@
     }
 
     #[allow(clippy::expect_used)]
-    fn encrypt(&self, tweak: Self::Tweak, block: &mut [u8; 16]) {
-        // we're encrypting precisely one block, so the block number won't advance, and ciphertext
-        // stealing will not be applied.
-        self.encrypt_data_unit(tweak, block).expect("One block is a valid size");
+    fn encrypt(&self, tweak: Self::Tweak, block: &mut [u8; BLOCK_SIZE]) {
+        // We're encrypting precisely one block, so the block number won't advance, and there is no
+        // need to apply ciphertext stealing
+        self.tweaked(tweak).encrypt_block(block)
     }
 }
 
@@ -311,8 +334,9 @@
     }
 
     #[allow(clippy::expect_used)]
-    fn decrypt(&self, tweak: Self::Tweak, block: &mut [u8; 16]) {
-        self.decrypt_data_unit(tweak, block).expect("One block is a valid size");
+    fn decrypt(&self, tweak: Self::Tweak, block: &mut [u8; BLOCK_SIZE]) {
+        // We don't need the ciphertext stealing here since the input size is always exactly one block
+        self.tweaked(tweak).decrypt_block(block)
     }
 }
 
@@ -341,6 +365,8 @@
     fn key_2(&self) -> &Self::BlockCipherKey;
 }
 
+const AES_128_KEY_SIZE: usize = 16;
+
 /// An XTS-AES-128 key.
 pub struct XtsAes128Key {
     key_1: <Self as XtsKey>::BlockCipherKey,
@@ -364,7 +390,7 @@
     type Error = XtsKeyTryFromSliceError;
 
     fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
-        try_split_concat_key::<16>(slice)
+        try_split_concat_key::<AES_128_KEY_SIZE>(slice)
             .map(|(key_1, key_2)| Self { key_1: key_1.into(), key_2: key_2.into() })
             .ok_or_else(XtsKeyTryFromSliceError::new)
     }
@@ -373,8 +399,10 @@
 #[allow(clippy::expect_used)]
 impl From<&[u8; 32]> for XtsAes128Key {
     fn from(array: &[u8; 32]) -> Self {
-        let arr1: [u8; 16] = array[..16].try_into().expect("array is correctly sized");
-        let arr2: [u8; 16] = array[16..].try_into().expect("array is correctly sized");
+        let arr1: [u8; AES_128_KEY_SIZE] =
+            array[..AES_128_KEY_SIZE].try_into().expect("array is correctly sized");
+        let arr2: [u8; AES_128_KEY_SIZE] =
+            array[AES_128_KEY_SIZE..].try_into().expect("array is correctly sized");
         Self {
             key_1: crypto_provider::aes::Aes128Key::from(arr1),
             key_2: crypto_provider::aes::Aes128Key::from(arr2),
@@ -402,6 +430,8 @@
     }
 }
 
+const AES_256_KEY_SIZE: usize = 32;
+
 /// An XTS-AES-256 key.
 pub struct XtsAes256Key {
     key_1: <Self as XtsKey>::BlockCipherKey,
@@ -425,7 +455,7 @@
     type Error = XtsKeyTryFromSliceError;
 
     fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
-        try_split_concat_key::<32>(slice)
+        try_split_concat_key::<AES_256_KEY_SIZE>(slice)
             .map(|(key_1, key_2)| Self { key_1: key_1.into(), key_2: key_2.into() })
             .ok_or_else(XtsKeyTryFromSliceError::new)
     }
@@ -434,8 +464,10 @@
 #[allow(clippy::expect_used)]
 impl From<&[u8; 64]> for XtsAes256Key {
     fn from(array: &[u8; 64]) -> Self {
-        let arr1: [u8; 32] = array[..32].try_into().expect("array is correctly sized");
-        let arr2: [u8; 32] = array[32..].try_into().expect("array is correctly sized");
+        let arr1: [u8; AES_256_KEY_SIZE] =
+            array[..AES_256_KEY_SIZE].try_into().expect("array is correctly sized");
+        let arr2: [u8; AES_256_KEY_SIZE] =
+            array[AES_256_KEY_SIZE..].try_into().expect("array is correctly sized");
         Self {
             key_1: crypto_provider::aes::Aes256Key::from(arr1),
             key_2: crypto_provider::aes::Aes256Key::from(arr2),
@@ -478,18 +510,18 @@
 /// The tweak for an XTS-AES cipher.
 #[derive(Clone)]
 pub struct Tweak {
-    bytes: crypto_provider::aes::AesBlock,
+    bytes: AesBlock,
 }
 
 impl Tweak {
     /// Little-endian content of the tweak.
-    pub fn le_bytes(&self) -> crypto_provider::aes::AesBlock {
+    pub fn le_bytes(&self) -> AesBlock {
         self.bytes
     }
 }
 
-impl From<crypto_provider::aes::AesBlock> for Tweak {
-    fn from(bytes: crypto_provider::aes::AesBlock) -> Self {
+impl From<AesBlock> for Tweak {
+    fn from(bytes: AesBlock) -> Self {
         Self { bytes }
     }
 }
@@ -506,15 +538,31 @@
     /// The block number inside the data unit. Should not exceed 2^20.
     block_num: u32,
     /// Original tweak multiplied by the primitive polynomial `block_num` times as per section 5.2
-    tweak: crypto_provider::aes::AesBlock,
+    tweak: AesBlock,
 }
 
 impl TweakState {
     /// Create a TweakState from the provided state with block_num = 0.
-    fn new(tweak: [u8; 16]) -> TweakState {
+    fn new(tweak: [u8; BLOCK_SIZE]) -> TweakState {
         TweakState { block_num: 0, tweak }
     }
 
+    /// Advance the tweak state in the data unit to the next block without encrypting
+    /// or decrypting the intermediate blocks.
+    #[allow(clippy::indexing_slicing)]
+    fn advance_to_next_block(&mut self) {
+        // Conceptual left shift across the bytes.
+        // Most significant byte: if shift would carry, XOR in the coefficients of primitive
+        // polynomial in F_2^128 (x^128 = x^7 + x^2 + x + 1) =>  0b1000_0111 in binary.
+        let mut target = [0_u8; BLOCK_SIZE];
+        target[0] = (self.tweak[0] << 1) ^ ((self.tweak[BLOCK_SIZE - 1] >> 7) * 0b1000_0111);
+        for (j, byte) in target.iter_mut().enumerate().skip(1) {
+            *byte = (self.tweak[j] << 1) ^ (self.tweak[j - 1] >> 7);
+        }
+        self.tweak = target;
+        self.block_num += 1;
+    }
+
     /// Advance the tweak state in the data unit to the `block_num`'th block without encrypting
     /// or decrypting the intermediate blocks.
     ///
@@ -522,39 +570,17 @@
     ///
     /// # Panics
     /// - If `block_num` is less than the current block num
+    #[cfg(test)]
     fn advance_to_block(&mut self, block_num: u32) {
         // It's a programmer error; nothing to recover from
         assert!(self.block_num <= block_num);
 
-        let mut target = [0_u8; BLOCK_SIZE];
-
         // Multiply by the primitive polynomial as many times as needed, as per section 5.2
         // of IEEE spec
-        #[allow(clippy::expect_used)]
+        #[allow(clippy::indexing_slicing)]
         for _ in 0..(block_num - self.block_num) {
-            // Conceptual left shift across the bytes.
-            // Most significant byte: if shift would carry, XOR in the coefficients of primitive
-            // polynomial in F_2^128 (x^128 = x^7 + x^2 + x + 1 = 0) = 135 decimal.
-            // % 128 is compiled as & !128 (i.e. fast).
-            target[0] = (2
-                * (self.tweak.first().expect("aes block must have non zero length") % 128))
-                ^ (135
-                    * select_hi_bit(
-                        *self.tweak.get(15).expect("15 is a valid index in an aes block"),
-                    ));
-            // Remaining bytes
-            for (j, byte) in target.iter_mut().enumerate().skip(1) {
-                *byte = (2
-                    * (self.tweak.get(j).expect("j is always in range of block size") % 128))
-                    ^ select_hi_bit(
-                        *self.tweak.get(j - 1).expect("j > 0 always because of the .skip(1)"),
-                    );
-            }
-            self.tweak = target;
-            // no need to zero target as it will be overwritten completely next iteration
+            self.advance_to_next_block()
         }
-
-        self.block_num = block_num;
     }
 }
 
@@ -570,11 +596,11 @@
 
 impl<'a, A: Aes> XtsEncrypterTweaked<'a, A> {
     fn advance_to_next_block_num(&mut self) {
-        self.tweak_state.advance_to_block(self.tweak_state.block_num + 1)
+        self.tweak_state.advance_to_next_block()
     }
 
     /// Encrypt a block in place using the configured tweak and current block number.
-    fn encrypt_block(&self, block: &mut crypto_provider::aes::AesBlock) {
+    fn encrypt_block(&self, block: &mut AesBlock) {
         array_xor(block, &self.tweak_state.tweak);
         self.enc_cipher.encrypt(block);
         array_xor(block, &self.tweak_state.tweak);
@@ -593,7 +619,7 @@
 
 impl<'a, A: Aes> XtsDecrypterTweaked<'a, A> {
     fn advance_to_next_block_num(&mut self) {
-        self.tweak_state.advance_to_block(self.tweak_state.block_num + 1)
+        self.tweak_state.advance_to_next_block()
     }
 
     /// Get the current tweak state -- useful if needed to reset to an earlier block num.
@@ -601,11 +627,11 @@
         self.tweak_state.clone()
     }
 
-    /// Set the tweak to a state captured via [current_tweak].
+    /// Set the tweak to a state captured via [`current_tweak`](XtsDecrypterTweaked::current_tweak).
     fn set_tweak(&mut self, tweak_state: TweakState) {
         self.tweak_state = tweak_state;
     }
-    fn decrypt_block(&self, block: &mut crypto_provider::aes::AesBlock) {
+    fn decrypt_block(&self, block: &mut AesBlock) {
         // CC = C ^ T
         array_xor(block, &self.tweak_state.tweak);
         // PP = decrypt CC
@@ -617,7 +643,7 @@
 
 /// Calculate `base = base ^ rhs` for each byte.
 #[allow(clippy::expect_used)]
-fn array_xor(base: &mut crypto_provider::aes::AesBlock, rhs: &crypto_provider::aes::AesBlock) {
+fn array_xor(base: &mut AesBlock, rhs: &AesBlock) {
     // hopefully this gets done smartly by the compiler (intel pxor, arm veorq, or equivalent).
     // This seems to happen in practice at opt level 3: https://gcc.godbolt.org/z/qvjE8joMv
     for i in 0..BLOCK_SIZE {
@@ -626,12 +652,6 @@
     }
 }
 
-/// 1 if hi bit set, 0 if not.
-fn select_hi_bit(byte: u8) -> u8 {
-    // compiled as shr 7: https://gcc.godbolt.org/z/1rzvfshnx
-    byte / 128
-}
-
 fn try_split_concat_key<const N: usize>(slice: &[u8]) -> Option<([u8; N], [u8; N])> {
     slice.get(0..N).and_then(|slice| slice.try_into().ok()).and_then(|k1: [u8; N]| {
         slice.get(N..).and_then(|slice| slice.try_into().ok()).map(|k2: [u8; N]| (k1, k2))
diff --git a/nearby/presence/xts_aes/tests/tests.rs b/nearby/presence/xts_aes/tests/tests.rs
new file mode 100644
index 0000000..0452f06
--- /dev/null
+++ b/nearby/presence/xts_aes/tests/tests.rs
@@ -0,0 +1,117 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crypto_provider::aes::{Aes, BLOCK_SIZE};
+use crypto_provider::CryptoProvider;
+use crypto_provider_default::CryptoProviderImpl;
+use ldt_tbc::{TweakableBlockCipherDecrypter, TweakableBlockCipherEncrypter};
+use xts_aes::{XtsAes128Key, XtsAes256Key, XtsDecrypter, XtsEncrypter, XtsError, XtsKey};
+
+const MAX_XTS_SIZE: usize = (1 << 20) * BLOCK_SIZE;
+
+#[test]
+fn too_small_payload() {
+    let key = [0_u8; 32];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes128, _>(
+        &XtsAes128Key::from(&key),
+    );
+    let tweak = [0u8; 16];
+    let mut payload = [0u8; BLOCK_SIZE - 1];
+    assert_eq!(enc.encrypt_data_unit(tweak.into(), &mut payload), Err(XtsError::DataTooShort));
+    assert_eq!(dec.decrypt_data_unit(tweak.into(), &mut payload), Err(XtsError::DataTooShort));
+
+    let key = [0u8; 64];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes256, _>(
+        &XtsAes256Key::from(&key),
+    );
+    assert_eq!(enc.encrypt_data_unit(tweak.into(), &mut payload), Err(XtsError::DataTooShort));
+    assert_eq!(dec.decrypt_data_unit(tweak.into(), &mut payload), Err(XtsError::DataTooShort));
+}
+
+#[test]
+fn block_size_payload() {
+    let key = [0_u8; 32];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes128, _>(
+        &XtsAes128Key::from(&key),
+    );
+    let tweak = [0u8; 16];
+    let mut payload = [0u8; BLOCK_SIZE];
+    assert!(enc.encrypt_data_unit(tweak.into(), &mut payload).is_ok());
+    assert!(dec.decrypt_data_unit(tweak.into(), &mut payload).is_ok());
+
+    let key = [0u8; 64];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes256, _>(
+        &XtsAes256Key::from(&key),
+    );
+    assert!(enc.encrypt_data_unit(tweak.into(), &mut payload).is_ok());
+    assert!(dec.decrypt_data_unit(tweak.into(), &mut payload).is_ok());
+}
+
+#[test]
+fn max_xts_sized_payload() {
+    let key = [0_u8; 32];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes128, _>(
+        &XtsAes128Key::from(&key),
+    );
+    let tweak = [0u8; 16];
+    let mut payload = vec![0u8; MAX_XTS_SIZE];
+    assert!(enc.encrypt_data_unit(tweak.into(), payload.as_mut_slice()).is_ok());
+    assert!(dec.decrypt_data_unit(tweak.into(), payload.as_mut_slice()).is_ok());
+
+    let key = [0u8; 64];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes256, _>(
+        &XtsAes256Key::from(&key),
+    );
+    assert!(enc.encrypt_data_unit(tweak.into(), payload.as_mut_slice()).is_ok());
+    assert!(dec.decrypt_data_unit(tweak.into(), payload.as_mut_slice()).is_ok());
+}
+
+#[test]
+fn too_large_payload() {
+    let key = [0_u8; 32];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes128, _>(
+        &XtsAes128Key::from(&key),
+    );
+    let tweak = [0u8; 16];
+    let mut payload = vec![0u8; MAX_XTS_SIZE + 1];
+    assert_eq!(
+        enc.encrypt_data_unit(tweak.into(), payload.as_mut_slice()),
+        Err(XtsError::DataTooLong)
+    );
+    assert_eq!(
+        dec.decrypt_data_unit(tweak.into(), payload.as_mut_slice()),
+        Err(XtsError::DataTooLong)
+    );
+
+    let key = [0u8; 64];
+    let (enc, dec) = build_ciphers::<<CryptoProviderImpl as CryptoProvider>::Aes256, _>(
+        &XtsAes256Key::from(&key),
+    );
+    assert_eq!(
+        enc.encrypt_data_unit(tweak.into(), payload.as_mut_slice()),
+        Err(XtsError::DataTooLong)
+    );
+    assert_eq!(
+        dec.decrypt_data_unit(tweak.into(), payload.as_mut_slice()),
+        Err(XtsError::DataTooLong)
+    );
+}
+
+fn build_ciphers<A: Aes<Key = K::BlockCipherKey>, K: XtsKey + ldt_tbc::TweakableBlockCipherKey>(
+    key: &K,
+) -> (XtsEncrypter<A, K>, XtsDecrypter<A, K>) {
+    let enc = XtsEncrypter::<A, _>::new(key);
+    let dec = XtsDecrypter::<A, _>::new(key);
+    (enc, dec)
+}
diff --git a/nearby/presence/xts_aes/tests/wycheproof_test_vectors.rs b/nearby/presence/xts_aes/tests/wycheproof_test_vectors.rs
new file mode 100644
index 0000000..724e3bd
--- /dev/null
+++ b/nearby/presence/xts_aes/tests/wycheproof_test_vectors.rs
@@ -0,0 +1,77 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate core;
+
+use crypto_provider::CryptoProvider;
+use crypto_provider_default::CryptoProviderImpl;
+use ldt_tbc::TweakableBlockCipherDecrypter;
+use ldt_tbc::TweakableBlockCipherEncrypter;
+use ldt_tbc::TweakableBlockCipherKey;
+use wycheproof::cipher::TestGroup;
+use xts_aes::{self, XtsAes128Key, XtsAes256Key, XtsDecrypter, XtsEncrypter, XtsKey};
+
+#[test]
+fn run_wycheproof_vectors() {
+    run_test_vectors();
+}
+
+fn run_test_vectors() {
+    let test_set = wycheproof::cipher::TestSet::load(wycheproof::cipher::TestName::AesXts)
+        .expect("Should be able to load test set");
+
+    for test_group in test_set.test_groups {
+        match test_group.key_size {
+            256 => run_tests::<XtsAes128Key, <CryptoProviderImpl as CryptoProvider>::Aes128>(
+                test_group,
+            ),
+            512 => run_tests::<XtsAes256Key, <CryptoProviderImpl as CryptoProvider>::Aes256>(
+                test_group,
+            ),
+            _ => {}
+        }
+    }
+}
+
+fn run_tests<K, A>(test_group: TestGroup)
+where
+    K: XtsKey + TweakableBlockCipherKey,
+    A: crypto_provider::aes::Aes<Key = K::BlockCipherKey>,
+{
+    let mut buf = Vec::new();
+    for test in test_group.tests {
+        buf.clear();
+        let key = test.key.as_slice();
+        let iv = test.nonce.as_slice();
+        let msg = test.pt.as_slice();
+        let ct = test.ct.as_slice();
+
+        let mut block = [0u8; 16];
+        block[..iv.len()].copy_from_slice(iv);
+        let tweak = xts_aes::Tweak::from(block);
+
+        // test encryption
+        let xts_enc = XtsEncrypter::<A, _>::new(&K::try_from(key).unwrap());
+        buf.extend_from_slice(msg);
+        xts_enc.encrypt_data_unit(tweak.clone(), &mut buf).unwrap();
+        assert_eq!(ct, buf, "Test case id: {}", test.tc_id);
+
+        // test decryption
+        buf.clear();
+        let xts_dec = XtsDecrypter::<A, _>::new(&K::try_from(key).unwrap());
+        buf.extend_from_slice(ct);
+        xts_dec.decrypt_data_unit(tweak, &mut buf).unwrap();
+        assert_eq!(msg, buf, "Test case id: {}", test.tc_id);
+    }
+}
diff --git a/nearby/scripts/openssl-patches/0001-Apply-Android-changes.patch b/nearby/scripts/openssl-patches/0001-Apply-Android-changes.patch
deleted file mode 100644
index be6ca09..0000000
--- a/nearby/scripts/openssl-patches/0001-Apply-Android-changes.patch
+++ /dev/null
@@ -1,2712 +0,0 @@
-From 66e1daa9b4a345617bf9a090d56fa09ccd9b1d35 Mon Sep 17 00:00:00 2001
-From: Maurice Lam <yukl@google.com>
-Date: Wed, 26 Apr 2023 02:21:36 +0000
-Subject: [PATCH] Apply Android patches
-
----
- openssl/src/bio.rs           |    6 +-
- openssl/src/bn.rs            |    2 +-
- openssl/src/cipher.rs        |    4 +
- openssl/src/dh.rs            |    2 +-
- openssl/src/ec.rs            |   20 +
- openssl/src/encrypt.rs       |    4 +-
- openssl/src/hkdf.rs          |   89 ++
- openssl/src/hmac.rs          |  217 ++++
- openssl/src/lib.rs           |   12 +
- openssl/src/pkey.rs          |   20 +-
- openssl/src/sign.rs          |   10 +-
- openssl/src/symm.rs          |    7 +-
- openssl/src/x509/mod.rs      |   52 +-
- openssl/src/x509/mod.rs.orig | 1879 ++++++++++++++++++++++++++++++++++
- 14 files changed, 2292 insertions(+), 32 deletions(-)
- create mode 100644 openssl/src/hkdf.rs
- create mode 100644 openssl/src/hmac.rs
- create mode 100644 openssl/src/x509/mod.rs.orig
-
-diff --git a/openssl/src/bio.rs b/openssl/src/bio.rs
-index 6a72552a..03242188 100644
---- a/openssl/src/bio.rs
-+++ b/openssl/src/bio.rs
-@@ -4,7 +4,7 @@ use std::marker::PhantomData;
- use std::ptr;
- use std::slice;
- 
--use crate::cvt_p;
-+use crate::{cvt_p, SignedLenType};
- use crate::error::ErrorStack;
- 
- pub struct MemBioSlice<'a>(*mut ffi::BIO, PhantomData<&'a [u8]>);
-@@ -25,7 +25,7 @@ impl<'a> MemBioSlice<'a> {
-         let bio = unsafe {
-             cvt_p(BIO_new_mem_buf(
-                 buf.as_ptr() as *const _,
--                buf.len() as c_int,
-+                buf.len() as SignedLenType,
-             ))?
-         };
- 
-@@ -78,7 +78,7 @@ cfg_if! {
-         use ffi::BIO_new_mem_buf;
-     } else {
-         #[allow(bad_style)]
--        unsafe fn BIO_new_mem_buf(buf: *const ::libc::c_void, len: ::libc::c_int) -> *mut ffi::BIO {
-+        unsafe fn BIO_new_mem_buf(buf: *const ::libc::c_void, len: SignedLenType) -> *mut ffi::BIO {
-             ffi::BIO_new_mem_buf(buf as *mut _, len)
-         }
-     }
-diff --git a/openssl/src/bn.rs b/openssl/src/bn.rs
-index 0328730a..fe820a2c 100644
---- a/openssl/src/bn.rs
-+++ b/openssl/src/bn.rs
-@@ -814,7 +814,7 @@ impl BigNumRef {
-     /// assert_eq!(&bn_vec, &[0, 0, 0x45, 0x43]);
-     /// ```
-     #[corresponds(BN_bn2binpad)]
--    #[cfg(ossl110)]
-+    #[cfg(any(boringssl, ossl110))]
-     pub fn to_vec_padded(&self, pad_to: i32) -> Result<Vec<u8>, ErrorStack> {
-         let mut v = Vec::with_capacity(pad_to as usize);
-         unsafe {
-diff --git a/openssl/src/cipher.rs b/openssl/src/cipher.rs
-index aeedf459..570794ca 100644
---- a/openssl/src/cipher.rs
-+++ b/openssl/src/cipher.rs
-@@ -208,6 +208,7 @@ impl Cipher {
-         unsafe { CipherRef::from_ptr(ffi::EVP_aes_192_cfb1() as *mut _) }
-     }
- 
-+    #[cfg(not(boringssl))]
-     pub fn aes_192_cfb128() -> &'static CipherRef {
-         unsafe { CipherRef::from_ptr(ffi::EVP_aes_192_cfb128() as *mut _) }
-     }
-@@ -253,6 +254,7 @@ impl Cipher {
-         unsafe { CipherRef::from_ptr(ffi::EVP_aes_256_cfb1() as *mut _) }
-     }
- 
-+    #[cfg(not(boringssl))]
-     pub fn aes_256_cfb128() -> &'static CipherRef {
-         unsafe { CipherRef::from_ptr(ffi::EVP_aes_256_cfb128() as *mut _) }
-     }
-@@ -282,11 +284,13 @@ impl Cipher {
-     }
- 
-     #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
-+    #[cfg(not(boringssl))]
-     pub fn bf_cbc() -> &'static CipherRef {
-         unsafe { CipherRef::from_ptr(ffi::EVP_bf_cbc() as *mut _) }
-     }
- 
-     #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
-+    #[cfg(not(boringssl))]
-     pub fn bf_ecb() -> &'static CipherRef {
-         unsafe { CipherRef::from_ptr(ffi::EVP_bf_ecb() as *mut _) }
-     }
-diff --git a/openssl/src/dh.rs b/openssl/src/dh.rs
-index 12170b99..e781543e 100644
---- a/openssl/src/dh.rs
-+++ b/openssl/src/dh.rs
-@@ -239,7 +239,7 @@ where
- }
- 
- cfg_if! {
--    if #[cfg(any(ossl110, libressl270))] {
-+    if #[cfg(any(ossl110, libressl270, boringssl))] {
-         use ffi::{DH_set0_pqg, DH_get0_pqg, DH_get0_key, DH_set0_key};
-     } else {
-         #[allow(bad_style)]
-diff --git a/openssl/src/ec.rs b/openssl/src/ec.rs
-index 248ced3e..c56f5da7 100644
---- a/openssl/src/ec.rs
-+++ b/openssl/src/ec.rs
-@@ -954,6 +954,26 @@ impl EcKey<Private> {
-         EcKey<Private>,
-         ffi::d2i_ECPrivateKey
-     }
-+
-+    /// Decodes a DER-encoded elliptic curve private key structure for the specified curve.
-+    #[corresponds(EC_KEY_parse_private_key)]
-+    #[cfg(boringssl)]
-+    pub fn private_key_from_der_for_group(
-+        der: &[u8],
-+        group: &EcGroupRef,
-+    ) -> Result<EcKey<Private>, ErrorStack> {
-+        unsafe {
-+            let mut cbs = ffi::CBS {
-+                data: der.as_ptr(),
-+                len: der.len(),
-+            };
-+            cvt_p(ffi::EC_KEY_parse_private_key(
-+                &mut cbs as *mut ffi::CBS,
-+                group.as_ptr(),
-+            ))
-+            .map(|p| EcKey::from_ptr(p))
-+        }
-+    }
- }
- 
- impl<T> Clone for EcKey<T> {
-diff --git a/openssl/src/encrypt.rs b/openssl/src/encrypt.rs
-index d3db0fd4..19a9a459 100644
---- a/openssl/src/encrypt.rs
-+++ b/openssl/src/encrypt.rs
-@@ -148,7 +148,7 @@ impl<'a> Encrypter<'a> {
-     /// This corresponds to [`EVP_PKEY_CTX_set_rsa_oaep_md`].
-     ///
-     /// [`EVP_PKEY_CTX_set_rsa_oaep_md`]: https://www.openssl.org/docs/manmaster/man3/EVP_PKEY_CTX_set_rsa_oaep_md.html
--    #[cfg(any(ossl102, libressl310))]
-+    #[cfg(any(ossl102, libressl310, boringssl))]
-     pub fn set_rsa_oaep_md(&mut self, md: MessageDigest) -> Result<(), ErrorStack> {
-         unsafe {
-             cvt(ffi::EVP_PKEY_CTX_set_rsa_oaep_md(
-@@ -352,7 +352,7 @@ impl<'a> Decrypter<'a> {
-     /// This corresponds to [`EVP_PKEY_CTX_set_rsa_oaep_md`].
-     ///
-     /// [`EVP_PKEY_CTX_set_rsa_oaep_md`]: https://www.openssl.org/docs/manmaster/man3/EVP_PKEY_CTX_set_rsa_oaep_md.html
--    #[cfg(any(ossl102, libressl310))]
-+    #[cfg(any(ossl102, libressl310, boringssl))]
-     pub fn set_rsa_oaep_md(&mut self, md: MessageDigest) -> Result<(), ErrorStack> {
-         unsafe {
-             cvt(ffi::EVP_PKEY_CTX_set_rsa_oaep_md(
-diff --git a/openssl/src/hkdf.rs b/openssl/src/hkdf.rs
-new file mode 100644
-index 00000000..cc7e5b3a
---- /dev/null
-+++ b/openssl/src/hkdf.rs
-@@ -0,0 +1,89 @@
-+use crate::cvt;
-+use crate::error::ErrorStack;
-+use crate::md::MdRef;
-+use foreign_types::ForeignTypeRef;
-+use openssl_macros::corresponds;
-+
-+/// Computes HKDF (as specified by RFC 5869).
-+///
-+/// HKDF is an Extract-and-Expand algorithm. It does not do any key stretching,
-+/// and as such, is not suited to be used alone to generate a key from a
-+/// password.
-+#[corresponds(HKDF)]
-+#[inline]
-+pub fn hkdf(
-+    out_key: &mut [u8],
-+    md: &MdRef,
-+    secret: &[u8],
-+    salt: &[u8],
-+    info: &[u8],
-+) -> Result<(), ErrorStack> {
-+    unsafe {
-+        cvt(ffi::HKDF(
-+            out_key.as_mut_ptr(),
-+            out_key.len(),
-+            md.as_ptr(),
-+            secret.as_ptr(),
-+            secret.len(),
-+            salt.as_ptr(),
-+            salt.len(),
-+            info.as_ptr(),
-+            info.len(),
-+        ))?;
-+    }
-+
-+    Ok(())
-+}
-+
-+/// Computes a HKDF PRK (as specified by RFC 5869).
-+///
-+/// WARNING: This function orders the inputs differently from RFC 5869
-+/// specification. Double-check which parameter is the secret/IKM and which is
-+/// the salt when using.
-+#[corresponds(HKDF_extract)]
-+#[inline]
-+pub fn hkdf_extract<'a>(
-+    out_key: &'a mut [u8],
-+    md: &MdRef,
-+    secret: &[u8],
-+    salt: &[u8],
-+) -> Result<&'a [u8], ErrorStack> {
-+    let mut out_len = out_key.len();
-+    unsafe {
-+        cvt(ffi::HKDF_extract(
-+            out_key.as_mut_ptr(),
-+            &mut out_len,
-+            md.as_ptr(),
-+            secret.as_ptr(),
-+            secret.len(),
-+            salt.as_ptr(),
-+            salt.len(),
-+        ))?;
-+    }
-+
-+    Ok(&out_key[..out_len])
-+}
-+
-+/// Computes a HKDF OKM (as specified by RFC 5869).
-+#[corresponds(HKDF_expand)]
-+#[inline]
-+pub fn hkdf_expand(
-+    out_key: &mut [u8],
-+    md: &MdRef,
-+    prk: &[u8],
-+    info: &[u8],
-+) -> Result<(), ErrorStack> {
-+    unsafe {
-+        cvt(ffi::HKDF_expand(
-+            out_key.as_mut_ptr(),
-+            out_key.len(),
-+            md.as_ptr(),
-+            prk.as_ptr(),
-+            prk.len(),
-+            info.as_ptr(),
-+            info.len(),
-+        ))?;
-+    }
-+
-+    Ok(())
-+}
-diff --git a/openssl/src/hmac.rs b/openssl/src/hmac.rs
-new file mode 100644
-index 00000000..465781e2
---- /dev/null
-+++ b/openssl/src/hmac.rs
-@@ -0,0 +1,217 @@
-+use crate::error::ErrorStack;
-+use crate::md::MdRef;
-+use crate::{cvt, cvt_p};
-+use ffi::HMAC_CTX;
-+use foreign_types::ForeignTypeRef;
-+use libc::{c_uint, c_void};
-+use openssl_macros::corresponds;
-+use std::convert::TryFrom;
-+use std::ptr;
-+
-+/// Computes the HMAC as a one-shot operation.
-+///
-+/// Calculates the HMAC of data, using the given |key|
-+/// and hash function |md|, and returns the result re-using the space from
-+/// buffer |out|. On entry, |out| must contain at least |EVP_MD_size| bytes
-+/// of space. The actual length of the result is used to resize the returned
-+/// slice. An output size of |EVP_MAX_MD_SIZE| will always be large enough.
-+/// It returns a resized |out| or ErrorStack on error.
-+#[corresponds(HMAC)]
-+#[inline]
-+pub fn hmac<'a>(
-+    md: &MdRef,
-+    key: &[u8],
-+    data: &[u8],
-+    out: &'a mut [u8],
-+) -> Result<&'a [u8], ErrorStack> {
-+    assert!(out.len() >= md.size());
-+    let mut out_len = c_uint::try_from(out.len()).unwrap();
-+    unsafe {
-+        cvt_p(ffi::HMAC(
-+            md.as_ptr(),
-+            key.as_ptr() as *const c_void,
-+            key.len(),
-+            data.as_ptr(),
-+            data.len(),
-+            out.as_mut_ptr(),
-+            &mut out_len,
-+        ))?;
-+    }
-+    Ok(&out[..out_len as usize])
-+}
-+
-+/// A context object used to perform HMAC operations.
-+///
-+/// HMAC is a MAC (message authentication code), i.e. a keyed hash function used for message
-+/// authentication, which is based on a hash function.
-+///
-+/// Note: Only available in boringssl. For openssl, use `PKey::hmac` instead.
-+#[cfg(boringssl)]
-+pub struct HmacCtx {
-+    ctx: *mut HMAC_CTX,
-+    output_size: usize,
-+}
-+
-+#[cfg(boringssl)]
-+impl HmacCtx {
-+    /// Creates a new [HmacCtx] to use the hash function `md` and key `key`.
-+    #[corresponds(HMAC_Init_ex)]
-+    pub fn new(key: &[u8], md: &MdRef) -> Result<Self, ErrorStack> {
-+        unsafe {
-+            // Safety: If an error occurred, the resulting null from HMAC_CTX_new is converted into
-+            // ErrorStack in the returned result by `cvt_p`.
-+            let ctx = cvt_p(ffi::HMAC_CTX_new())?;
-+            // Safety:
-+            // - HMAC_Init_ex must be called with a context previously created with HMAC_CTX_new,
-+            //   which is the line above.
-+            // - HMAC_Init_ex may return an error if key is null but the md is different from
-+            //   before. This is avoided here since key is guaranteed to be non-null.
-+            cvt(ffi::HMAC_Init_ex(
-+                ctx,
-+                key.as_ptr() as *const c_void,
-+                key.len(),
-+                md.as_ptr(),
-+                ptr::null_mut(),
-+            ))?;
-+            Ok(Self {
-+                ctx,
-+                output_size: md.size(),
-+            })
-+        }
-+    }
-+
-+    /// `update` can be called repeatedly with chunks of the message `data` to be authenticated.
-+    #[corresponds(HMAC_Update)]
-+    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
-+        unsafe {
-+            // Safety: HMAC_Update returns 0 on error, and that is converted into ErrorStack in the
-+            // returned result by `cvt`.
-+            cvt(ffi::HMAC_Update(self.ctx, data.as_ptr(), data.len())).map(|_| ())
-+        }
-+    }
-+
-+    /// Finishes the HMAC process, and places the message authentication code in `output`.
-+    /// The number of bytes written to `output` is returned.
-+    ///
-+    /// # Panics
-+    ///
-+    /// Panics if the `output` is smaller than the required size. The output size is indicated by
-+    /// `md.size()` for the `Md` instance passed in [new]. An output size of |EVP_MAX_MD_SIZE| will
-+    /// always be large enough.
-+    #[corresponds(HMAC_Final)]
-+    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack> {
-+        assert!(output.len() >= self.output_size);
-+        unsafe {
-+            // Safety: The length assertion above makes sure that `HMAC_Final` will not write longer
-+            // than the length of `output`.
-+            let mut size: c_uint = 0;
-+            cvt(ffi::HMAC_Final(
-+                self.ctx,
-+                output.as_mut_ptr(),
-+                &mut size as *mut c_uint,
-+            ))
-+            .map(|_| size as usize)
-+        }
-+    }
-+}
-+
-+impl Drop for HmacCtx {
-+    #[corresponds(HMAC_CTX_free)]
-+    fn drop(&mut self) {
-+        unsafe {
-+            ffi::HMAC_CTX_free(self.ctx);
-+        }
-+    }
-+}
-+
-+#[cfg(test)]
-+mod tests {
-+    use super::*;
-+    use crate::md::Md;
-+
-+    const SHA_256_DIGEST_SIZE: usize = 32;
-+
-+    #[test]
-+    fn hmac_sha256_test() {
-+        let expected_hmac = [
-+            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
-+            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
-+            0x2e, 0x32, 0xcf, 0xf7,
-+        ];
-+        let mut out: [u8; SHA_256_DIGEST_SIZE] = [0; SHA_256_DIGEST_SIZE];
-+        let key: [u8; 20] = [0x0b; 20];
-+        let data = b"Hi There";
-+        let hmac_result =
-+            hmac(Md::sha256(), &key, data, &mut out).expect("Couldn't calculate sha256 hmac");
-+        assert_eq!(&hmac_result, &expected_hmac);
-+    }
-+
-+    #[test]
-+    #[should_panic]
-+    fn hmac_sha256_output_too_short() {
-+        let mut out = vec![0_u8; 1];
-+        let key: [u8; 20] = [0x0b; 20];
-+        let data = b"Hi There";
-+        hmac(Md::sha256(), &key, data, &mut out).expect("Couldn't calculate sha256 hmac");
-+    }
-+
-+    #[test]
-+    fn hmac_sha256_test_big_buffer() {
-+        let expected_hmac = [
-+            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
-+            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
-+            0x2e, 0x32, 0xcf, 0xf7,
-+        ];
-+        let mut out: [u8; 100] = [0; 100];
-+        let key: [u8; 20] = [0x0b; 20];
-+        let data = b"Hi There";
-+        let hmac_result =
-+            hmac(Md::sha256(), &key, data, &mut out).expect("Couldn't calculate sha256 hmac");
-+        assert_eq!(hmac_result.len(), SHA_256_DIGEST_SIZE);
-+        assert_eq!(&hmac_result, &expected_hmac);
-+    }
-+
-+    #[test]
-+    fn hmac_sha256_update_test() {
-+        let expected_hmac = [
-+            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
-+            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
-+            0x2e, 0x32, 0xcf, 0xf7,
-+        ];
-+        let mut out: [u8; SHA_256_DIGEST_SIZE] = [0; SHA_256_DIGEST_SIZE];
-+        let key: [u8; 20] = [0x0b; 20];
-+        let data = b"Hi There";
-+        let mut hmac_ctx = HmacCtx::new(&key, Md::sha256()).unwrap();
-+        hmac_ctx.update(data).unwrap();
-+        let size = hmac_ctx.finalize(&mut out).unwrap();
-+        assert_eq!(&out, &expected_hmac);
-+        assert_eq!(size, SHA_256_DIGEST_SIZE);
-+    }
-+
-+    #[test]
-+    fn hmac_sha256_update_chunks_test() {
-+        let expected_hmac = [
-+            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
-+            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
-+            0x2e, 0x32, 0xcf, 0xf7,
-+        ];
-+        let mut out: [u8; SHA_256_DIGEST_SIZE] = [0; SHA_256_DIGEST_SIZE];
-+        let key: [u8; 20] = [0x0b; 20];
-+        let mut hmac_ctx = HmacCtx::new(&key, Md::sha256()).unwrap();
-+        hmac_ctx.update(b"Hi").unwrap();
-+        hmac_ctx.update(b" There").unwrap();
-+        let size = hmac_ctx.finalize(&mut out).unwrap();
-+        assert_eq!(&out, &expected_hmac);
-+        assert_eq!(size, SHA_256_DIGEST_SIZE);
-+    }
-+
-+    #[test]
-+    #[should_panic]
-+    fn hmac_sha256_update_output_too_short() {
-+        let mut out = vec![0_u8; 1];
-+        let key: [u8; 20] = [0x0b; 20];
-+        let mut hmac_ctx = HmacCtx::new(&key, Md::sha256()).unwrap();
-+        hmac_ctx.update(b"Hi There").unwrap();
-+        hmac_ctx.finalize(&mut out).unwrap();
-+    }
-+}
-diff --git a/openssl/src/lib.rs b/openssl/src/lib.rs
-index 035c90c6..02a51dbb 100644
---- a/openssl/src/lib.rs
-+++ b/openssl/src/lib.rs
-@@ -120,6 +120,9 @@
- #![doc(html_root_url = "https://docs.rs/openssl/0.10")]
- #![warn(rust_2018_idioms)]
- 
-+#[cfg(all(soong, boringssl))]
-+extern crate bssl_ffi as ffi;
-+
- #[doc(inline)]
- pub use ffi::init;
- 
-@@ -155,6 +158,10 @@ pub mod ex_data;
- #[cfg(not(any(libressl, ossl300)))]
- pub mod fips;
- pub mod hash;
-+#[cfg(boringssl)]
-+pub mod hkdf;
-+#[cfg(boringssl)]
-+pub mod hmac;
- #[cfg(ossl300)]
- pub mod lib_ctx;
- pub mod md;
-@@ -189,6 +196,11 @@ type LenType = libc::size_t;
- #[cfg(not(boringssl))]
- type LenType = libc::c_int;
- 
-+#[cfg(boringssl)]
-+type SignedLenType = libc::ssize_t;
-+#[cfg(not(boringssl))]
-+type SignedLenType = libc::c_int;
-+
- #[inline]
- fn cvt_p<T>(r: *mut T) -> Result<*mut T, ErrorStack> {
-     if r.is_null() {
-diff --git a/openssl/src/pkey.rs b/openssl/src/pkey.rs
-index 780bd637..92daa882 100644
---- a/openssl/src/pkey.rs
-+++ b/openssl/src/pkey.rs
-@@ -47,7 +47,7 @@ use crate::dh::Dh;
- use crate::dsa::Dsa;
- use crate::ec::EcKey;
- use crate::error::ErrorStack;
--#[cfg(ossl110)]
-+#[cfg(any(boringssl, ossl110))]
- use crate::pkey_ctx::PkeyCtx;
- use crate::rsa::Rsa;
- use crate::symm::Cipher;
-@@ -89,11 +89,11 @@ impl Id {
-     #[cfg(ossl110)]
-     pub const HKDF: Id = Id(ffi::EVP_PKEY_HKDF);
- 
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub const ED25519: Id = Id(ffi::EVP_PKEY_ED25519);
-     #[cfg(ossl111)]
-     pub const ED448: Id = Id(ffi::EVP_PKEY_ED448);
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub const X25519: Id = Id(ffi::EVP_PKEY_X25519);
-     #[cfg(ossl111)]
-     pub const X448: Id = Id(ffi::EVP_PKEY_X448);
-@@ -252,7 +252,7 @@ where
-     /// This function only works for algorithms that support raw public keys.
-     /// Currently this is: [`Id::X25519`], [`Id::ED25519`], [`Id::X448`] or [`Id::ED448`].
-     #[corresponds(EVP_PKEY_get_raw_public_key)]
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn raw_public_key(&self) -> Result<Vec<u8>, ErrorStack> {
-         unsafe {
-             let mut len = 0;
-@@ -303,7 +303,7 @@ where
-     /// This function only works for algorithms that support raw private keys.
-     /// Currently this is: [`Id::HMAC`], [`Id::X25519`], [`Id::ED25519`], [`Id::X448`] or [`Id::ED448`].
-     #[corresponds(EVP_PKEY_get_raw_private_key)]
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn raw_private_key(&self) -> Result<Vec<u8>, ErrorStack> {
-         unsafe {
-             let mut len = 0;
-@@ -503,7 +503,7 @@ impl PKey<Private> {
-         ctx.keygen()
-     }
- 
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     fn generate_eddsa(id: Id) -> Result<PKey<Private>, ErrorStack> {
-         let mut ctx = PkeyCtx::new_id(id)?;
-         ctx.keygen_init()?;
-@@ -533,7 +533,7 @@ impl PKey<Private> {
-     /// assert_eq!(secret.len(), 32);
-     /// # Ok(()) }
-     /// ```
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn generate_x25519() -> Result<PKey<Private>, ErrorStack> {
-         PKey::generate_eddsa(Id::X25519)
-     }
-@@ -587,7 +587,7 @@ impl PKey<Private> {
-     /// assert_eq!(signature.len(), 64);
-     /// # Ok(()) }
-     /// ```
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn generate_ed25519() -> Result<PKey<Private>, ErrorStack> {
-         PKey::generate_eddsa(Id::ED25519)
-     }
-@@ -737,7 +737,7 @@ impl PKey<Private> {
-     ///
-     /// Algorithm types that support raw private keys are HMAC, X25519, ED25519, X448 or ED448
-     #[corresponds(EVP_PKEY_new_raw_private_key)]
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn private_key_from_raw_bytes(
-         bytes: &[u8],
-         key_type: Id,
-@@ -778,7 +778,7 @@ impl PKey<Public> {
-     ///
-     /// Algorithm types that support raw public keys are X25519, ED25519, X448 or ED448
-     #[corresponds(EVP_PKEY_new_raw_public_key)]
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn public_key_from_raw_bytes(
-         bytes: &[u8],
-         key_type: Id,
-diff --git a/openssl/src/sign.rs b/openssl/src/sign.rs
-index 9cfda481..e5b17a87 100644
---- a/openssl/src/sign.rs
-+++ b/openssl/src/sign.rs
-@@ -290,7 +290,7 @@ impl<'a> Signer<'a> {
-         self.len_intern()
-     }
- 
--    #[cfg(not(ossl111))]
-+    #[cfg(not(any(boringssl, ossl111)))]
-     fn len_intern(&self) -> Result<usize, ErrorStack> {
-         unsafe {
-             let mut len = 0;
-@@ -303,7 +303,7 @@ impl<'a> Signer<'a> {
-         }
-     }
- 
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     fn len_intern(&self) -> Result<usize, ErrorStack> {
-         unsafe {
-             let mut len = 0;
-@@ -360,7 +360,7 @@ impl<'a> Signer<'a> {
-     /// OpenSSL documentation at [`EVP_DigestSign`].
-     ///
-     /// [`EVP_DigestSign`]: https://www.openssl.org/docs/man1.1.1/man3/EVP_DigestSign.html
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn sign_oneshot(
-         &mut self,
-         sig_buf: &mut [u8],
-@@ -382,7 +382,7 @@ impl<'a> Signer<'a> {
-     /// Returns the signature.
-     ///
-     /// This is a simple convenience wrapper over `len` and `sign_oneshot`.
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn sign_oneshot_to_vec(&mut self, data_buf: &[u8]) -> Result<Vec<u8>, ErrorStack> {
-         let mut sig_buf = vec![0; self.len()?];
-         let len = self.sign_oneshot(&mut sig_buf, data_buf)?;
-@@ -596,7 +596,7 @@ impl<'a> Verifier<'a> {
-     /// OpenSSL documentation at [`EVP_DigestVerify`].
-     ///
-     /// [`EVP_DigestVerify`]: https://www.openssl.org/docs/man1.1.1/man3/EVP_DigestVerify.html
--    #[cfg(ossl111)]
-+    #[cfg(any(boringssl, ossl111))]
-     pub fn verify_oneshot(&mut self, signature: &[u8], buf: &[u8]) -> Result<bool, ErrorStack> {
-         unsafe {
-             let r = ffi::EVP_DigestVerify(
-diff --git a/openssl/src/symm.rs b/openssl/src/symm.rs
-index 911a7ab2..75b0365c 100644
---- a/openssl/src/symm.rs
-+++ b/openssl/src/symm.rs
-@@ -119,6 +119,7 @@ impl Cipher {
-         unsafe { Cipher(ffi::EVP_aes_128_cfb1()) }
-     }
- 
-+    #[cfg(not(boringssl))]
-     pub fn aes_128_cfb128() -> Cipher {
-         unsafe { Cipher(ffi::EVP_aes_128_cfb128()) }
-     }
-@@ -164,6 +165,7 @@ impl Cipher {
-         unsafe { Cipher(ffi::EVP_aes_192_cfb1()) }
-     }
- 
-+    #[cfg(not(boringssl))]
-     pub fn aes_192_cfb128() -> Cipher {
-         unsafe { Cipher(ffi::EVP_aes_192_cfb128()) }
-     }
-@@ -214,6 +216,7 @@ impl Cipher {
-         unsafe { Cipher(ffi::EVP_aes_256_cfb1()) }
-     }
- 
-+    #[cfg(not(boringssl))]
-     pub fn aes_256_cfb128() -> Cipher {
-         unsafe { Cipher(ffi::EVP_aes_256_cfb128()) }
-     }
-@@ -242,12 +245,12 @@ impl Cipher {
-         unsafe { Cipher(ffi::EVP_aes_256_ocb()) }
-     }
- 
--    #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
-+    #[cfg(not(any(boringssl, osslconf = "OPENSSL_NO_BF")))]
-     pub fn bf_cbc() -> Cipher {
-         unsafe { Cipher(ffi::EVP_bf_cbc()) }
-     }
- 
--    #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
-+    #[cfg(not(any(boringssl, osslconf = "OPENSSL_NO_BF")))]
-     pub fn bf_ecb() -> Cipher {
-         unsafe { Cipher(ffi::EVP_bf_ecb()) }
-     }
-diff --git a/openssl/src/x509/mod.rs b/openssl/src/x509/mod.rs
-index 940c8c9c..f9477e4a 100644
---- a/openssl/src/x509/mod.rs
-+++ b/openssl/src/x509/mod.rs
-@@ -356,6 +356,19 @@ impl X509Builder {
-         unsafe { cvt(ffi::X509_sign(self.0.as_ptr(), key.as_ptr(), hash.as_ptr())).map(|_| ()) }
-     }
- 
-+    /// Signs the certificate with a private key but without a digest.
-+    ///
-+    /// This is the only way to sign with Ed25519 keys as BoringSSL doesn't support the null
-+    /// message digest.
-+    #[cfg(boringssl)]
-+    #[corresponds(X509_sign)]
-+    pub fn sign_without_digest<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
-+    where
-+        T: HasPrivate,
-+    {
-+        unsafe { cvt(ffi::X509_sign(self.0.as_ptr(), key.as_ptr(), ptr::null())).map(|_| ()) }
-+    }
-+
-     /// Consumes the builder, returning the certificate.
-     pub fn build(self) -> X509 {
-         self.0
-@@ -898,13 +911,13 @@ impl X509NameBuilder {
-     pub fn append_entry_by_text(&mut self, field: &str, value: &str) -> Result<(), ErrorStack> {
-         unsafe {
-             let field = CString::new(field).unwrap();
--            assert!(value.len() <= c_int::max_value() as usize);
-+            assert!(value.len() <= isize::max_value() as usize);
-             cvt(ffi::X509_NAME_add_entry_by_txt(
-                 self.0.as_ptr(),
-                 field.as_ptr() as *mut _,
-                 ffi::MBSTRING_UTF8,
-                 value.as_ptr(),
--                value.len() as c_int,
-+                value.len() as isize,
-                 -1,
-                 0,
-             ))
-@@ -925,13 +938,13 @@ impl X509NameBuilder {
-     ) -> Result<(), ErrorStack> {
-         unsafe {
-             let field = CString::new(field).unwrap();
--            assert!(value.len() <= c_int::max_value() as usize);
-+            assert!(value.len() <= isize::max_value() as usize);
-             cvt(ffi::X509_NAME_add_entry_by_txt(
-                 self.0.as_ptr(),
-                 field.as_ptr() as *mut _,
-                 ty.as_raw(),
-                 value.as_ptr(),
--                value.len() as c_int,
-+                value.len() as isize,
-                 -1,
-                 0,
-             ))
-@@ -946,13 +959,13 @@ impl X509NameBuilder {
-     /// [`X509_NAME_add_entry_by_NID`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_add_entry_by_NID.html
-     pub fn append_entry_by_nid(&mut self, field: Nid, value: &str) -> Result<(), ErrorStack> {
-         unsafe {
--            assert!(value.len() <= c_int::max_value() as usize);
-+            assert!(value.len() <= isize::max_value() as usize);
-             cvt(ffi::X509_NAME_add_entry_by_NID(
-                 self.0.as_ptr(),
-                 field.as_raw(),
-                 ffi::MBSTRING_UTF8,
-                 value.as_ptr() as *mut _,
--                value.len() as c_int,
-+                value.len() as isize,
-                 -1,
-                 0,
-             ))
-@@ -972,13 +985,13 @@ impl X509NameBuilder {
-         ty: Asn1Type,
-     ) -> Result<(), ErrorStack> {
-         unsafe {
--            assert!(value.len() <= c_int::max_value() as usize);
-+            assert!(value.len() <= isize::max_value() as usize);
-             cvt(ffi::X509_NAME_add_entry_by_NID(
-                 self.0.as_ptr(),
-                 field.as_raw(),
-                 ty.as_raw(),
-                 value.as_ptr() as *mut _,
--                value.len() as c_int,
-+                value.len() as isize,
-                 -1,
-                 0,
-             ))
-@@ -1286,6 +1299,29 @@ impl X509ReqBuilder {
-         }
-     }
- 
-+    /// Sign the request using a private key without a digest.
-+    ///
-+    /// This is the only way to sign with Ed25519 keys as BoringSSL doesn't support the null
-+    /// message digest.
-+    ///
-+    /// This corresponds to [`X509_REQ_sign`].
-+    ///
-+    /// [`X509_REQ_sign`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_REQ_sign.html
-+    #[cfg(boringssl)]
-+    pub fn sign_without_digest<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
-+    where
-+        T: HasPrivate,
-+    {
-+        unsafe {
-+            cvt(ffi::X509_REQ_sign(
-+                self.0.as_ptr(),
-+                key.as_ptr(),
-+                ptr::null(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-     /// Returns the `X509Req`.
-     pub fn build(self) -> X509Req {
-         self.0
-diff --git a/openssl/src/x509/mod.rs.orig b/openssl/src/x509/mod.rs.orig
-new file mode 100644
-index 00000000..34b86c10
---- /dev/null
-+++ b/openssl/src/x509/mod.rs.orig
-@@ -0,0 +1,1879 @@
-+//! The standard defining the format of public key certificates.
-+//!
-+//! An `X509` certificate binds an identity to a public key, and is either
-+//! signed by a certificate authority (CA) or self-signed. An entity that gets
-+//! a hold of a certificate can both verify your identity (via a CA) and encrypt
-+//! data with the included public key. `X509` certificates are used in many
-+//! Internet protocols, including SSL/TLS, which is the basis for HTTPS,
-+//! the secure protocol for browsing the web.
-+
-+use cfg_if::cfg_if;
-+use foreign_types::{ForeignType, ForeignTypeRef, Opaque};
-+use libc::{c_int, c_long, c_uint};
-+use std::cmp::{self, Ordering};
-+use std::convert::TryFrom;
-+use std::error::Error;
-+use std::ffi::{CStr, CString};
-+use std::fmt;
-+use std::marker::PhantomData;
-+use std::mem;
-+use std::net::IpAddr;
-+use std::path::Path;
-+use std::ptr;
-+use std::slice;
-+use std::str;
-+
-+use crate::asn1::{
-+    Asn1BitStringRef, Asn1IntegerRef, Asn1ObjectRef, Asn1StringRef, Asn1TimeRef, Asn1Type,
-+};
-+use crate::bio::MemBioSlice;
-+use crate::conf::ConfRef;
-+use crate::error::ErrorStack;
-+use crate::ex_data::Index;
-+use crate::hash::{DigestBytes, MessageDigest};
-+use crate::nid::Nid;
-+use crate::pkey::{HasPrivate, HasPublic, PKey, PKeyRef, Public};
-+use crate::ssl::SslRef;
-+use crate::stack::{Stack, StackRef, Stackable};
-+use crate::string::OpensslString;
-+use crate::util::{ForeignTypeExt, ForeignTypeRefExt};
-+use crate::{cvt, cvt_n, cvt_p};
-+use openssl_macros::corresponds;
-+
-+#[cfg(any(ossl102, libressl261))]
-+pub mod verify;
-+
-+pub mod extension;
-+pub mod store;
-+
-+#[cfg(test)]
-+mod tests;
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_STORE_CTX;
-+    fn drop = ffi::X509_STORE_CTX_free;
-+
-+    /// An `X509` certificate store context.
-+    pub struct X509StoreContext;
-+
-+    /// A reference to an [`X509StoreContext`].
-+    pub struct X509StoreContextRef;
-+}
-+
-+impl X509StoreContext {
-+    /// Returns the index which can be used to obtain a reference to the `Ssl` associated with a
-+    /// context.
-+    #[corresponds(SSL_get_ex_data_X509_STORE_CTX_idx)]
-+    pub fn ssl_idx() -> Result<Index<X509StoreContext, SslRef>, ErrorStack> {
-+        unsafe { cvt_n(ffi::SSL_get_ex_data_X509_STORE_CTX_idx()).map(|idx| Index::from_raw(idx)) }
-+    }
-+
-+    /// Creates a new `X509StoreContext` instance.
-+    #[corresponds(X509_STORE_CTX_new)]
-+    pub fn new() -> Result<X509StoreContext, ErrorStack> {
-+        unsafe {
-+            ffi::init();
-+            cvt_p(ffi::X509_STORE_CTX_new()).map(X509StoreContext)
-+        }
-+    }
-+}
-+
-+impl X509StoreContextRef {
-+    /// Returns application data pertaining to an `X509` store context.
-+    #[corresponds(X509_STORE_CTX_get_ex_data)]
-+    pub fn ex_data<T>(&self, index: Index<X509StoreContext, T>) -> Option<&T> {
-+        unsafe {
-+            let data = ffi::X509_STORE_CTX_get_ex_data(self.as_ptr(), index.as_raw());
-+            if data.is_null() {
-+                None
-+            } else {
-+                Some(&*(data as *const T))
-+            }
-+        }
-+    }
-+
-+    /// Returns the error code of the context.
-+    #[corresponds(X509_STORE_CTX_get_error)]
-+    pub fn error(&self) -> X509VerifyResult {
-+        unsafe { X509VerifyResult::from_raw(ffi::X509_STORE_CTX_get_error(self.as_ptr())) }
-+    }
-+
-+    /// Initializes this context with the given certificate, certificates chain and certificate
-+    /// store. After initializing the context, the `with_context` closure is called with the prepared
-+    /// context. As long as the closure is running, the context stays initialized and can be used
-+    /// to e.g. verify a certificate. The context will be cleaned up, after the closure finished.
-+    ///
-+    /// * `trust` - The certificate store with the trusted certificates.
-+    /// * `cert` - The certificate that should be verified.
-+    /// * `cert_chain` - The certificates chain.
-+    /// * `with_context` - The closure that is called with the initialized context.
-+    ///
-+    /// This corresponds to [`X509_STORE_CTX_init`] before calling `with_context` and to
-+    /// [`X509_STORE_CTX_cleanup`] after calling `with_context`.
-+    ///
-+    /// [`X509_STORE_CTX_init`]:  https://www.openssl.org/docs/manmaster/crypto/X509_STORE_CTX_init.html
-+    /// [`X509_STORE_CTX_cleanup`]:  https://www.openssl.org/docs/manmaster/crypto/X509_STORE_CTX_cleanup.html
-+    pub fn init<F, T>(
-+        &mut self,
-+        trust: &store::X509StoreRef,
-+        cert: &X509Ref,
-+        cert_chain: &StackRef<X509>,
-+        with_context: F,
-+    ) -> Result<T, ErrorStack>
-+    where
-+        F: FnOnce(&mut X509StoreContextRef) -> Result<T, ErrorStack>,
-+    {
-+        struct Cleanup<'a>(&'a mut X509StoreContextRef);
-+
-+        impl<'a> Drop for Cleanup<'a> {
-+            fn drop(&mut self) {
-+                unsafe {
-+                    ffi::X509_STORE_CTX_cleanup(self.0.as_ptr());
-+                }
-+            }
-+        }
-+
-+        unsafe {
-+            cvt(ffi::X509_STORE_CTX_init(
-+                self.as_ptr(),
-+                trust.as_ptr(),
-+                cert.as_ptr(),
-+                cert_chain.as_ptr(),
-+            ))?;
-+
-+            let cleanup = Cleanup(self);
-+            with_context(cleanup.0)
-+        }
-+    }
-+
-+    /// Verifies the stored certificate.
-+    ///
-+    /// Returns `true` if verification succeeds. The `error` method will return the specific
-+    /// validation error if the certificate was not valid.
-+    ///
-+    /// This will only work inside of a call to `init`.
-+    #[corresponds(X509_verify_cert)]
-+    pub fn verify_cert(&mut self) -> Result<bool, ErrorStack> {
-+        unsafe { cvt_n(ffi::X509_verify_cert(self.as_ptr())).map(|n| n != 0) }
-+    }
-+
-+    /// Set the error code of the context.
-+    #[corresponds(X509_STORE_CTX_set_error)]
-+    pub fn set_error(&mut self, result: X509VerifyResult) {
-+        unsafe {
-+            ffi::X509_STORE_CTX_set_error(self.as_ptr(), result.as_raw());
-+        }
-+    }
-+
-+    /// Returns a reference to the certificate which caused the error or None if
-+    /// no certificate is relevant to the error.
-+    #[corresponds(X509_STORE_CTX_get_current_cert)]
-+    pub fn current_cert(&self) -> Option<&X509Ref> {
-+        unsafe {
-+            let ptr = ffi::X509_STORE_CTX_get_current_cert(self.as_ptr());
-+            X509Ref::from_const_ptr_opt(ptr)
-+        }
-+    }
-+
-+    /// Returns a non-negative integer representing the depth in the certificate
-+    /// chain where the error occurred. If it is zero it occurred in the end
-+    /// entity certificate, one if it is the certificate which signed the end
-+    /// entity certificate and so on.
-+    #[corresponds(X509_STORE_CTX_get_error_depth)]
-+    pub fn error_depth(&self) -> u32 {
-+        unsafe { ffi::X509_STORE_CTX_get_error_depth(self.as_ptr()) as u32 }
-+    }
-+
-+    /// Returns a reference to a complete valid `X509` certificate chain.
-+    #[corresponds(X509_STORE_CTX_get0_chain)]
-+    pub fn chain(&self) -> Option<&StackRef<X509>> {
-+        unsafe {
-+            let chain = X509_STORE_CTX_get0_chain(self.as_ptr());
-+
-+            if chain.is_null() {
-+                None
-+            } else {
-+                Some(StackRef::from_ptr(chain))
-+            }
-+        }
-+    }
-+}
-+
-+/// A builder used to construct an `X509`.
-+pub struct X509Builder(X509);
-+
-+impl X509Builder {
-+    /// Creates a new builder.
-+    #[corresponds(X509_new)]
-+    pub fn new() -> Result<X509Builder, ErrorStack> {
-+        unsafe {
-+            ffi::init();
-+            cvt_p(ffi::X509_new()).map(|p| X509Builder(X509(p)))
-+        }
-+    }
-+
-+    /// Sets the notAfter constraint on the certificate.
-+    #[corresponds(X509_set1_notAfter)]
-+    pub fn set_not_after(&mut self, not_after: &Asn1TimeRef) -> Result<(), ErrorStack> {
-+        unsafe { cvt(X509_set1_notAfter(self.0.as_ptr(), not_after.as_ptr())).map(|_| ()) }
-+    }
-+
-+    /// Sets the notBefore constraint on the certificate.
-+    #[corresponds(X509_set1_notBefore)]
-+    pub fn set_not_before(&mut self, not_before: &Asn1TimeRef) -> Result<(), ErrorStack> {
-+        unsafe { cvt(X509_set1_notBefore(self.0.as_ptr(), not_before.as_ptr())).map(|_| ()) }
-+    }
-+
-+    /// Sets the version of the certificate.
-+    ///
-+    /// Note that the version is zero-indexed; that is, a certificate corresponding to version 3 of
-+    /// the X.509 standard should pass `2` to this method.
-+    #[corresponds(X509_set_version)]
-+    #[allow(clippy::useless_conversion)]
-+    pub fn set_version(&mut self, version: i32) -> Result<(), ErrorStack> {
-+        unsafe { cvt(ffi::X509_set_version(self.0.as_ptr(), version as c_long)).map(|_| ()) }
-+    }
-+
-+    /// Sets the serial number of the certificate.
-+    #[corresponds(X509_set_serialNumber)]
-+    pub fn set_serial_number(&mut self, serial_number: &Asn1IntegerRef) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_set_serialNumber(
-+                self.0.as_ptr(),
-+                serial_number.as_ptr(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Sets the issuer name of the certificate.
-+    #[corresponds(X509_set_issuer_name)]
-+    pub fn set_issuer_name(&mut self, issuer_name: &X509NameRef) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_set_issuer_name(
-+                self.0.as_ptr(),
-+                issuer_name.as_ptr(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Sets the subject name of the certificate.
-+    ///
-+    /// When building certificates, the `C`, `ST`, and `O` options are common when using the openssl command line tools.
-+    /// The `CN` field is used for the common name, such as a DNS name.
-+    ///
-+    /// ```
-+    /// use openssl::x509::{X509, X509NameBuilder};
-+    ///
-+    /// let mut x509_name = openssl::x509::X509NameBuilder::new().unwrap();
-+    /// x509_name.append_entry_by_text("C", "US").unwrap();
-+    /// x509_name.append_entry_by_text("ST", "CA").unwrap();
-+    /// x509_name.append_entry_by_text("O", "Some organization").unwrap();
-+    /// x509_name.append_entry_by_text("CN", "www.example.com").unwrap();
-+    /// let x509_name = x509_name.build();
-+    ///
-+    /// let mut x509 = openssl::x509::X509::builder().unwrap();
-+    /// x509.set_subject_name(&x509_name).unwrap();
-+    /// ```
-+    #[corresponds(X509_set_subject_name)]
-+    pub fn set_subject_name(&mut self, subject_name: &X509NameRef) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_set_subject_name(
-+                self.0.as_ptr(),
-+                subject_name.as_ptr(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Sets the public key associated with the certificate.
-+    #[corresponds(X509_set_pubkey)]
-+    pub fn set_pubkey<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
-+    where
-+        T: HasPublic,
-+    {
-+        unsafe { cvt(ffi::X509_set_pubkey(self.0.as_ptr(), key.as_ptr())).map(|_| ()) }
-+    }
-+
-+    /// Returns a context object which is needed to create certain X509 extension values.
-+    ///
-+    /// Set `issuer` to `None` if the certificate will be self-signed.
-+    #[corresponds(X509V3_set_ctx)]
-+    pub fn x509v3_context<'a>(
-+        &'a self,
-+        issuer: Option<&'a X509Ref>,
-+        conf: Option<&'a ConfRef>,
-+    ) -> X509v3Context<'a> {
-+        unsafe {
-+            let mut ctx = mem::zeroed();
-+
-+            let issuer = match issuer {
-+                Some(issuer) => issuer.as_ptr(),
-+                None => self.0.as_ptr(),
-+            };
-+            let subject = self.0.as_ptr();
-+            ffi::X509V3_set_ctx(
-+                &mut ctx,
-+                issuer,
-+                subject,
-+                ptr::null_mut(),
-+                ptr::null_mut(),
-+                0,
-+            );
-+
-+            // nodb case taken care of since we zeroed ctx above
-+            if let Some(conf) = conf {
-+                ffi::X509V3_set_nconf(&mut ctx, conf.as_ptr());
-+            }
-+
-+            X509v3Context(ctx, PhantomData)
-+        }
-+    }
-+
-+    /// Adds an X509 extension value to the certificate.
-+    ///
-+    /// This works just as `append_extension` except it takes ownership of the `X509Extension`.
-+    pub fn append_extension(&mut self, extension: X509Extension) -> Result<(), ErrorStack> {
-+        self.append_extension2(&extension)
-+    }
-+
-+    /// Adds an X509 extension value to the certificate.
-+    #[corresponds(X509_add_ext)]
-+    pub fn append_extension2(&mut self, extension: &X509ExtensionRef) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_add_ext(self.0.as_ptr(), extension.as_ptr(), -1))?;
-+            Ok(())
-+        }
-+    }
-+
-+    /// Signs the certificate with a private key.
-+    #[corresponds(X509_sign)]
-+    pub fn sign<T>(&mut self, key: &PKeyRef<T>, hash: MessageDigest) -> Result<(), ErrorStack>
-+    where
-+        T: HasPrivate,
-+    {
-+        unsafe { cvt(ffi::X509_sign(self.0.as_ptr(), key.as_ptr(), hash.as_ptr())).map(|_| ()) }
-+    }
-+
-+    /// Signs the certificate with a private key but without a digest.
-+    ///
-+    /// This is the only way to sign with Ed25519 keys as BoringSSL doesn't support the null
-+    /// message digest.
-+    #[cfg(boringssl)]
-+    #[corresponds(X509_sign)]
-+    pub fn sign_without_digest<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
-+    where
-+        T: HasPrivate,
-+    {
-+        unsafe { cvt(ffi::X509_sign(self.0.as_ptr(), key.as_ptr(), ptr::null())).map(|_| ()) }
-+    }
-+
-+    /// Consumes the builder, returning the certificate.
-+    pub fn build(self) -> X509 {
-+        self.0
-+    }
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509;
-+    fn drop = ffi::X509_free;
-+
-+    /// An `X509` public key certificate.
-+    pub struct X509;
-+    /// Reference to `X509`.
-+    pub struct X509Ref;
-+}
-+
-+#[cfg(boringssl)]
-+type X509LenTy = c_uint;
-+#[cfg(not(boringssl))]
-+type X509LenTy = c_int;
-+
-+impl X509Ref {
-+    /// Returns this certificate's subject name.
-+    #[corresponds(X509_get_subject_name)]
-+    pub fn subject_name(&self) -> &X509NameRef {
-+        unsafe {
-+            let name = ffi::X509_get_subject_name(self.as_ptr());
-+            X509NameRef::from_const_ptr_opt(name).expect("subject name must not be null")
-+        }
-+    }
-+
-+    /// Returns the hash of the certificates subject
-+    #[corresponds(X509_subject_name_hash)]
-+    pub fn subject_name_hash(&self) -> u32 {
-+        unsafe { ffi::X509_subject_name_hash(self.as_ptr()) as u32 }
-+    }
-+
-+    /// Returns this certificate's issuer name.
-+    #[corresponds(X509_get_issuer_name)]
-+    pub fn issuer_name(&self) -> &X509NameRef {
-+        unsafe {
-+            let name = ffi::X509_get_issuer_name(self.as_ptr());
-+            X509NameRef::from_const_ptr_opt(name).expect("issuer name must not be null")
-+        }
-+    }
-+
-+    /// Returns the hash of the certificates issuer
-+    #[corresponds(X509_issuer_name_hash)]
-+    pub fn issuer_name_hash(&self) -> u32 {
-+        unsafe { ffi::X509_issuer_name_hash(self.as_ptr()) as u32 }
-+    }
-+
-+    /// Returns this certificate's subject alternative name entries, if they exist.
-+    #[corresponds(X509_get_ext_d2i)]
-+    pub fn subject_alt_names(&self) -> Option<Stack<GeneralName>> {
-+        unsafe {
-+            let stack = ffi::X509_get_ext_d2i(
-+                self.as_ptr(),
-+                ffi::NID_subject_alt_name,
-+                ptr::null_mut(),
-+                ptr::null_mut(),
-+            );
-+            Stack::from_ptr_opt(stack as *mut _)
-+        }
-+    }
-+
-+    /// Returns this certificate's issuer alternative name entries, if they exist.
-+    #[corresponds(X509_get_ext_d2i)]
-+    pub fn issuer_alt_names(&self) -> Option<Stack<GeneralName>> {
-+        unsafe {
-+            let stack = ffi::X509_get_ext_d2i(
-+                self.as_ptr(),
-+                ffi::NID_issuer_alt_name,
-+                ptr::null_mut(),
-+                ptr::null_mut(),
-+            );
-+            Stack::from_ptr_opt(stack as *mut _)
-+        }
-+    }
-+
-+    /// Returns this certificate's [`authority information access`] entries, if they exist.
-+    ///
-+    /// [`authority information access`]: https://tools.ietf.org/html/rfc5280#section-4.2.2.1
-+    #[corresponds(X509_get_ext_d2i)]
-+    pub fn authority_info(&self) -> Option<Stack<AccessDescription>> {
-+        unsafe {
-+            let stack = ffi::X509_get_ext_d2i(
-+                self.as_ptr(),
-+                ffi::NID_info_access,
-+                ptr::null_mut(),
-+                ptr::null_mut(),
-+            );
-+            Stack::from_ptr_opt(stack as *mut _)
-+        }
-+    }
-+
-+    #[corresponds(X509_get_pubkey)]
-+    pub fn public_key(&self) -> Result<PKey<Public>, ErrorStack> {
-+        unsafe {
-+            let pkey = cvt_p(ffi::X509_get_pubkey(self.as_ptr()))?;
-+            Ok(PKey::from_ptr(pkey))
-+        }
-+    }
-+
-+    /// Returns a digest of the DER representation of the certificate.
-+    #[corresponds(X509_digest)]
-+    pub fn digest(&self, hash_type: MessageDigest) -> Result<DigestBytes, ErrorStack> {
-+        unsafe {
-+            let mut digest = DigestBytes {
-+                buf: [0; ffi::EVP_MAX_MD_SIZE as usize],
-+                len: ffi::EVP_MAX_MD_SIZE as usize,
-+            };
-+            let mut len = ffi::EVP_MAX_MD_SIZE as c_uint;
-+            cvt(ffi::X509_digest(
-+                self.as_ptr(),
-+                hash_type.as_ptr(),
-+                digest.buf.as_mut_ptr() as *mut _,
-+                &mut len,
-+            ))?;
-+            digest.len = len as usize;
-+
-+            Ok(digest)
-+        }
-+    }
-+
-+    #[deprecated(since = "0.10.9", note = "renamed to digest")]
-+    pub fn fingerprint(&self, hash_type: MessageDigest) -> Result<Vec<u8>, ErrorStack> {
-+        self.digest(hash_type).map(|b| b.to_vec())
-+    }
-+
-+    /// Returns the certificate's Not After validity period.
-+    #[corresponds(X509_getm_notAfter)]
-+    pub fn not_after(&self) -> &Asn1TimeRef {
-+        unsafe {
-+            let date = X509_getm_notAfter(self.as_ptr());
-+            Asn1TimeRef::from_const_ptr_opt(date).expect("not_after must not be null")
-+        }
-+    }
-+
-+    /// Returns the certificate's Not Before validity period.
-+    #[corresponds(X509_getm_notBefore)]
-+    pub fn not_before(&self) -> &Asn1TimeRef {
-+        unsafe {
-+            let date = X509_getm_notBefore(self.as_ptr());
-+            Asn1TimeRef::from_const_ptr_opt(date).expect("not_before must not be null")
-+        }
-+    }
-+
-+    /// Returns the certificate's signature
-+    #[corresponds(X509_get0_signature)]
-+    pub fn signature(&self) -> &Asn1BitStringRef {
-+        unsafe {
-+            let mut signature = ptr::null();
-+            X509_get0_signature(&mut signature, ptr::null_mut(), self.as_ptr());
-+            Asn1BitStringRef::from_const_ptr_opt(signature).expect("signature must not be null")
-+        }
-+    }
-+
-+    /// Returns the certificate's signature algorithm.
-+    #[corresponds(X509_get0_signature)]
-+    pub fn signature_algorithm(&self) -> &X509AlgorithmRef {
-+        unsafe {
-+            let mut algor = ptr::null();
-+            X509_get0_signature(ptr::null_mut(), &mut algor, self.as_ptr());
-+            X509AlgorithmRef::from_const_ptr_opt(algor)
-+                .expect("signature algorithm must not be null")
-+        }
-+    }
-+
-+    /// Returns the list of OCSP responder URLs specified in the certificate's Authority Information
-+    /// Access field.
-+    #[corresponds(X509_get1_ocsp)]
-+    pub fn ocsp_responders(&self) -> Result<Stack<OpensslString>, ErrorStack> {
-+        unsafe { cvt_p(ffi::X509_get1_ocsp(self.as_ptr())).map(|p| Stack::from_ptr(p)) }
-+    }
-+
-+    /// Checks that this certificate issued `subject`.
-+    #[corresponds(X509_check_issued)]
-+    pub fn issued(&self, subject: &X509Ref) -> X509VerifyResult {
-+        unsafe {
-+            let r = ffi::X509_check_issued(self.as_ptr(), subject.as_ptr());
-+            X509VerifyResult::from_raw(r)
-+        }
-+    }
-+
-+    /// Returns certificate version. If this certificate has no explicit version set, it defaults to
-+    /// version 1.
-+    ///
-+    /// Note that `0` return value stands for version 1, `1` for version 2 and so on.
-+    #[corresponds(X509_get_version)]
-+    #[cfg(ossl110)]
-+    pub fn version(&self) -> i32 {
-+        unsafe { ffi::X509_get_version(self.as_ptr()) as i32 }
-+    }
-+
-+    /// Check if the certificate is signed using the given public key.
-+    ///
-+    /// Only the signature is checked: no other checks (such as certificate chain validity)
-+    /// are performed.
-+    ///
-+    /// Returns `true` if verification succeeds.
-+    #[corresponds(X509_verify)]
-+    pub fn verify<T>(&self, key: &PKeyRef<T>) -> Result<bool, ErrorStack>
-+    where
-+        T: HasPublic,
-+    {
-+        unsafe { cvt_n(ffi::X509_verify(self.as_ptr(), key.as_ptr())).map(|n| n != 0) }
-+    }
-+
-+    /// Returns this certificate's serial number.
-+    #[corresponds(X509_get_serialNumber)]
-+    pub fn serial_number(&self) -> &Asn1IntegerRef {
-+        unsafe {
-+            let r = ffi::X509_get_serialNumber(self.as_ptr());
-+            Asn1IntegerRef::from_const_ptr_opt(r).expect("serial number must not be null")
-+        }
-+    }
-+
-+    to_pem! {
-+        /// Serializes the certificate into a PEM-encoded X509 structure.
-+        ///
-+        /// The output will have a header of `-----BEGIN CERTIFICATE-----`.
-+        #[corresponds(PEM_write_bio_X509)]
-+        to_pem,
-+        ffi::PEM_write_bio_X509
-+    }
-+
-+    to_der! {
-+        /// Serializes the certificate into a DER-encoded X509 structure.
-+        #[corresponds(i2d_X509)]
-+        to_der,
-+        ffi::i2d_X509
-+    }
-+
-+    to_pem! {
-+        /// Converts the certificate to human readable text.
-+        #[corresponds(X509_print)]
-+        to_text,
-+        ffi::X509_print
-+    }
-+}
-+
-+impl ToOwned for X509Ref {
-+    type Owned = X509;
-+
-+    fn to_owned(&self) -> X509 {
-+        unsafe {
-+            X509_up_ref(self.as_ptr());
-+            X509::from_ptr(self.as_ptr())
-+        }
-+    }
-+}
-+
-+impl Ord for X509Ref {
-+    fn cmp(&self, other: &Self) -> cmp::Ordering {
-+        // X509_cmp returns a number <0 for less than, 0 for equal and >0 for greater than.
-+        // It can't fail if both pointers are valid, which we know is true.
-+        let cmp = unsafe { ffi::X509_cmp(self.as_ptr(), other.as_ptr()) };
-+        cmp.cmp(&0)
-+    }
-+}
-+
-+impl PartialOrd for X509Ref {
-+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
-+        Some(self.cmp(other))
-+    }
-+}
-+
-+impl PartialOrd<X509> for X509Ref {
-+    fn partial_cmp(&self, other: &X509) -> Option<cmp::Ordering> {
-+        <X509Ref as PartialOrd<X509Ref>>::partial_cmp(self, other)
-+    }
-+}
-+
-+impl PartialEq for X509Ref {
-+    fn eq(&self, other: &Self) -> bool {
-+        self.cmp(other) == cmp::Ordering::Equal
-+    }
-+}
-+
-+impl PartialEq<X509> for X509Ref {
-+    fn eq(&self, other: &X509) -> bool {
-+        <X509Ref as PartialEq<X509Ref>>::eq(self, other)
-+    }
-+}
-+
-+impl Eq for X509Ref {}
-+
-+impl X509 {
-+    /// Returns a new builder.
-+    pub fn builder() -> Result<X509Builder, ErrorStack> {
-+        X509Builder::new()
-+    }
-+
-+    from_pem! {
-+        /// Deserializes a PEM-encoded X509 structure.
-+        ///
-+        /// The input should have a header of `-----BEGIN CERTIFICATE-----`.
-+        #[corresponds(PEM_read_bio_X509)]
-+        from_pem,
-+        X509,
-+        ffi::PEM_read_bio_X509
-+    }
-+
-+    from_der! {
-+        /// Deserializes a DER-encoded X509 structure.
-+        #[corresponds(d2i_X509)]
-+        from_der,
-+        X509,
-+        ffi::d2i_X509
-+    }
-+
-+    /// Deserializes a list of PEM-formatted certificates.
-+    #[corresponds(PEM_read_bio_X509)]
-+    pub fn stack_from_pem(pem: &[u8]) -> Result<Vec<X509>, ErrorStack> {
-+        unsafe {
-+            ffi::init();
-+            let bio = MemBioSlice::new(pem)?;
-+
-+            let mut certs = vec![];
-+            loop {
-+                let r =
-+                    ffi::PEM_read_bio_X509(bio.as_ptr(), ptr::null_mut(), None, ptr::null_mut());
-+                if r.is_null() {
-+                    let err = ffi::ERR_peek_last_error();
-+                    if ffi::ERR_GET_LIB(err) as X509LenTy == ffi::ERR_LIB_PEM
-+                        && ffi::ERR_GET_REASON(err) == ffi::PEM_R_NO_START_LINE
-+                    {
-+                        ffi::ERR_clear_error();
-+                        break;
-+                    }
-+
-+                    return Err(ErrorStack::get());
-+                } else {
-+                    certs.push(X509(r));
-+                }
-+            }
-+
-+            Ok(certs)
-+        }
-+    }
-+}
-+
-+impl Clone for X509 {
-+    fn clone(&self) -> X509 {
-+        X509Ref::to_owned(self)
-+    }
-+}
-+
-+impl fmt::Debug for X509 {
-+    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
-+        let serial = match &self.serial_number().to_bn() {
-+            Ok(bn) => match bn.to_hex_str() {
-+                Ok(hex) => hex.to_string(),
-+                Err(_) => "".to_string(),
-+            },
-+            Err(_) => "".to_string(),
-+        };
-+        let mut debug_struct = formatter.debug_struct("X509");
-+        debug_struct.field("serial_number", &serial);
-+        debug_struct.field("signature_algorithm", &self.signature_algorithm().object());
-+        debug_struct.field("issuer", &self.issuer_name());
-+        debug_struct.field("subject", &self.subject_name());
-+        if let Some(subject_alt_names) = &self.subject_alt_names() {
-+            debug_struct.field("subject_alt_names", subject_alt_names);
-+        }
-+        debug_struct.field("not_before", &self.not_before());
-+        debug_struct.field("not_after", &self.not_after());
-+
-+        if let Ok(public_key) = &self.public_key() {
-+            debug_struct.field("public_key", public_key);
-+        };
-+        // TODO: Print extensions once they are supported on the X509 struct.
-+
-+        debug_struct.finish()
-+    }
-+}
-+
-+impl AsRef<X509Ref> for X509Ref {
-+    fn as_ref(&self) -> &X509Ref {
-+        self
-+    }
-+}
-+
-+impl Stackable for X509 {
-+    type StackType = ffi::stack_st_X509;
-+}
-+
-+impl Ord for X509 {
-+    fn cmp(&self, other: &Self) -> cmp::Ordering {
-+        X509Ref::cmp(self, other)
-+    }
-+}
-+
-+impl PartialOrd for X509 {
-+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
-+        X509Ref::partial_cmp(self, other)
-+    }
-+}
-+
-+impl PartialOrd<X509Ref> for X509 {
-+    fn partial_cmp(&self, other: &X509Ref) -> Option<cmp::Ordering> {
-+        X509Ref::partial_cmp(self, other)
-+    }
-+}
-+
-+impl PartialEq for X509 {
-+    fn eq(&self, other: &Self) -> bool {
-+        X509Ref::eq(self, other)
-+    }
-+}
-+
-+impl PartialEq<X509Ref> for X509 {
-+    fn eq(&self, other: &X509Ref) -> bool {
-+        X509Ref::eq(self, other)
-+    }
-+}
-+
-+impl Eq for X509 {}
-+
-+/// A context object required to construct certain `X509` extension values.
-+pub struct X509v3Context<'a>(ffi::X509V3_CTX, PhantomData<(&'a X509Ref, &'a ConfRef)>);
-+
-+impl<'a> X509v3Context<'a> {
-+    pub fn as_ptr(&self) -> *mut ffi::X509V3_CTX {
-+        &self.0 as *const _ as *mut _
-+    }
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_EXTENSION;
-+    fn drop = ffi::X509_EXTENSION_free;
-+
-+    /// Permit additional fields to be added to an `X509` v3 certificate.
-+    pub struct X509Extension;
-+    /// Reference to `X509Extension`.
-+    pub struct X509ExtensionRef;
-+}
-+
-+impl Stackable for X509Extension {
-+    type StackType = ffi::stack_st_X509_EXTENSION;
-+}
-+
-+impl X509Extension {
-+    /// Constructs an X509 extension value. See `man x509v3_config` for information on supported
-+    /// names and their value formats.
-+    ///
-+    /// Some extension types, such as `subjectAlternativeName`, require an `X509v3Context` to be
-+    /// provided.
-+    ///
-+    /// See the extension module for builder types which will construct certain common extensions.
-+    pub fn new(
-+        conf: Option<&ConfRef>,
-+        context: Option<&X509v3Context<'_>>,
-+        name: &str,
-+        value: &str,
-+    ) -> Result<X509Extension, ErrorStack> {
-+        let name = CString::new(name).unwrap();
-+        let value = CString::new(value).unwrap();
-+        unsafe {
-+            ffi::init();
-+            let conf = conf.map_or(ptr::null_mut(), ConfRef::as_ptr);
-+            let context = context.map_or(ptr::null_mut(), X509v3Context::as_ptr);
-+            let name = name.as_ptr() as *mut _;
-+            let value = value.as_ptr() as *mut _;
-+
-+            cvt_p(ffi::X509V3_EXT_nconf(conf, context, name, value)).map(X509Extension)
-+        }
-+    }
-+
-+    /// Constructs an X509 extension value. See `man x509v3_config` for information on supported
-+    /// extensions and their value formats.
-+    ///
-+    /// Some extension types, such as `nid::SUBJECT_ALTERNATIVE_NAME`, require an `X509v3Context` to
-+    /// be provided.
-+    ///
-+    /// See the extension module for builder types which will construct certain common extensions.
-+    pub fn new_nid(
-+        conf: Option<&ConfRef>,
-+        context: Option<&X509v3Context<'_>>,
-+        name: Nid,
-+        value: &str,
-+    ) -> Result<X509Extension, ErrorStack> {
-+        let value = CString::new(value).unwrap();
-+        unsafe {
-+            ffi::init();
-+            let conf = conf.map_or(ptr::null_mut(), ConfRef::as_ptr);
-+            let context = context.map_or(ptr::null_mut(), X509v3Context::as_ptr);
-+            let name = name.as_raw();
-+            let value = value.as_ptr() as *mut _;
-+
-+            cvt_p(ffi::X509V3_EXT_nconf_nid(conf, context, name, value)).map(X509Extension)
-+        }
-+    }
-+
-+    /// Adds an alias for an extension
-+    ///
-+    /// # Safety
-+    ///
-+    /// This method modifies global state without locking and therefore is not thread safe
-+    #[corresponds(X509V3_EXT_add_alias)]
-+    pub unsafe fn add_alias(to: Nid, from: Nid) -> Result<(), ErrorStack> {
-+        ffi::init();
-+        cvt(ffi::X509V3_EXT_add_alias(to.as_raw(), from.as_raw())).map(|_| ())
-+    }
-+}
-+
-+/// A builder used to construct an `X509Name`.
-+pub struct X509NameBuilder(X509Name);
-+
-+impl X509NameBuilder {
-+    /// Creates a new builder.
-+    pub fn new() -> Result<X509NameBuilder, ErrorStack> {
-+        unsafe {
-+            ffi::init();
-+            cvt_p(ffi::X509_NAME_new()).map(|p| X509NameBuilder(X509Name(p)))
-+        }
-+    }
-+
-+    /// Add a name entry
-+    #[corresponds(X509_NAME_add_entry)]
-+    #[cfg(any(ossl101, libressl350))]
-+    pub fn append_entry(&mut self, ne: &X509NameEntryRef) -> std::result::Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_NAME_add_entry(
-+                self.0.as_ptr(),
-+                ne.as_ptr(),
-+                -1,
-+                0,
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Add a field entry by str.
-+    ///
-+    /// This corresponds to [`X509_NAME_add_entry_by_txt`].
-+    ///
-+    /// [`X509_NAME_add_entry_by_txt`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_add_entry_by_txt.html
-+    pub fn append_entry_by_text(&mut self, field: &str, value: &str) -> Result<(), ErrorStack> {
-+        unsafe {
-+            let field = CString::new(field).unwrap();
-+            assert!(value.len() <= c_int::max_value() as usize);
-+            cvt(ffi::X509_NAME_add_entry_by_txt(
-+                self.0.as_ptr(),
-+                field.as_ptr() as *mut _,
-+                ffi::MBSTRING_UTF8,
-+                value.as_ptr(),
-+                value.len() as c_int,
-+                -1,
-+                0,
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Add a field entry by str with a specific type.
-+    ///
-+    /// This corresponds to [`X509_NAME_add_entry_by_txt`].
-+    ///
-+    /// [`X509_NAME_add_entry_by_txt`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_add_entry_by_txt.html
-+    pub fn append_entry_by_text_with_type(
-+        &mut self,
-+        field: &str,
-+        value: &str,
-+        ty: Asn1Type,
-+    ) -> Result<(), ErrorStack> {
-+        unsafe {
-+            let field = CString::new(field).unwrap();
-+            assert!(value.len() <= c_int::max_value() as usize);
-+            cvt(ffi::X509_NAME_add_entry_by_txt(
-+                self.0.as_ptr(),
-+                field.as_ptr() as *mut _,
-+                ty.as_raw(),
-+                value.as_ptr(),
-+                value.len() as c_int,
-+                -1,
-+                0,
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Add a field entry by NID.
-+    ///
-+    /// This corresponds to [`X509_NAME_add_entry_by_NID`].
-+    ///
-+    /// [`X509_NAME_add_entry_by_NID`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_add_entry_by_NID.html
-+    pub fn append_entry_by_nid(&mut self, field: Nid, value: &str) -> Result<(), ErrorStack> {
-+        unsafe {
-+            assert!(value.len() <= c_int::max_value() as usize);
-+            cvt(ffi::X509_NAME_add_entry_by_NID(
-+                self.0.as_ptr(),
-+                field.as_raw(),
-+                ffi::MBSTRING_UTF8,
-+                value.as_ptr() as *mut _,
-+                value.len() as c_int,
-+                -1,
-+                0,
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Add a field entry by NID with a specific type.
-+    ///
-+    /// This corresponds to [`X509_NAME_add_entry_by_NID`].
-+    ///
-+    /// [`X509_NAME_add_entry_by_NID`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_add_entry_by_NID.html
-+    pub fn append_entry_by_nid_with_type(
-+        &mut self,
-+        field: Nid,
-+        value: &str,
-+        ty: Asn1Type,
-+    ) -> Result<(), ErrorStack> {
-+        unsafe {
-+            assert!(value.len() <= c_int::max_value() as usize);
-+            cvt(ffi::X509_NAME_add_entry_by_NID(
-+                self.0.as_ptr(),
-+                field.as_raw(),
-+                ty.as_raw(),
-+                value.as_ptr() as *mut _,
-+                value.len() as c_int,
-+                -1,
-+                0,
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Return an `X509Name`.
-+    pub fn build(self) -> X509Name {
-+        self.0
-+    }
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_NAME;
-+    fn drop = ffi::X509_NAME_free;
-+
-+    /// The names of an `X509` certificate.
-+    pub struct X509Name;
-+    /// Reference to `X509Name`.
-+    pub struct X509NameRef;
-+}
-+
-+impl X509Name {
-+    /// Returns a new builder.
-+    pub fn builder() -> Result<X509NameBuilder, ErrorStack> {
-+        X509NameBuilder::new()
-+    }
-+
-+    /// Loads subject names from a file containing PEM-formatted certificates.
-+    ///
-+    /// This is commonly used in conjunction with `SslContextBuilder::set_client_ca_list`.
-+    pub fn load_client_ca_file<P: AsRef<Path>>(file: P) -> Result<Stack<X509Name>, ErrorStack> {
-+        let file = CString::new(file.as_ref().as_os_str().to_str().unwrap()).unwrap();
-+        unsafe { cvt_p(ffi::SSL_load_client_CA_file(file.as_ptr())).map(|p| Stack::from_ptr(p)) }
-+    }
-+
-+    from_der! {
-+        /// Deserializes a DER-encoded X509 name structure.
-+        ///
-+        /// This corresponds to [`d2i_X509_NAME`].
-+        ///
-+        /// [`d2i_X509_NAME`]: https://www.openssl.org/docs/manmaster/man3/d2i_X509_NAME.html
-+        from_der,
-+        X509Name,
-+        ffi::d2i_X509_NAME
-+    }
-+}
-+
-+impl Stackable for X509Name {
-+    type StackType = ffi::stack_st_X509_NAME;
-+}
-+
-+impl X509NameRef {
-+    /// Returns the name entries by the nid.
-+    pub fn entries_by_nid(&self, nid: Nid) -> X509NameEntries<'_> {
-+        X509NameEntries {
-+            name: self,
-+            nid: Some(nid),
-+            loc: -1,
-+        }
-+    }
-+
-+    /// Returns an iterator over all `X509NameEntry` values
-+    pub fn entries(&self) -> X509NameEntries<'_> {
-+        X509NameEntries {
-+            name: self,
-+            nid: None,
-+            loc: -1,
-+        }
-+    }
-+
-+    /// Compare two names, like [`Ord`] but it may fail.
-+    ///
-+    /// With OpenSSL versions from 3.0.0 this may return an error if the underlying `X509_NAME_cmp`
-+    /// call fails.
-+    /// For OpenSSL versions before 3.0.0 it will never return an error, but due to a bug it may
-+    /// spuriously return `Ordering::Less` if the `X509_NAME_cmp` call fails.
-+    #[corresponds(X509_NAME_cmp)]
-+    pub fn try_cmp(&self, other: &X509NameRef) -> Result<Ordering, ErrorStack> {
-+        let cmp = unsafe { ffi::X509_NAME_cmp(self.as_ptr(), other.as_ptr()) };
-+        if cfg!(ossl300) && cmp == -2 {
-+            return Err(ErrorStack::get());
-+        }
-+        Ok(cmp.cmp(&0))
-+    }
-+
-+    /// Copies the name to a new `X509Name`.
-+    #[corresponds(X509_NAME_dup)]
-+    #[cfg(any(boringssl, ossl110, libressl270))]
-+    pub fn to_owned(&self) -> Result<X509Name, ErrorStack> {
-+        unsafe { cvt_p(ffi::X509_NAME_dup(self.as_ptr())).map(|n| X509Name::from_ptr(n)) }
-+    }
-+
-+    to_der! {
-+        /// Serializes the certificate into a DER-encoded X509 name structure.
-+        ///
-+        /// This corresponds to [`i2d_X509_NAME`].
-+        ///
-+        /// [`i2d_X509_NAME`]: https://www.openssl.org/docs/manmaster/crypto/i2d_X509_NAME.html
-+        to_der,
-+        ffi::i2d_X509_NAME
-+    }
-+}
-+
-+impl fmt::Debug for X509NameRef {
-+    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
-+        formatter.debug_list().entries(self.entries()).finish()
-+    }
-+}
-+
-+/// A type to destructure and examine an `X509Name`.
-+pub struct X509NameEntries<'a> {
-+    name: &'a X509NameRef,
-+    nid: Option<Nid>,
-+    loc: c_int,
-+}
-+
-+impl<'a> Iterator for X509NameEntries<'a> {
-+    type Item = &'a X509NameEntryRef;
-+
-+    fn next(&mut self) -> Option<&'a X509NameEntryRef> {
-+        unsafe {
-+            match self.nid {
-+                Some(nid) => {
-+                    // There is a `Nid` specified to search for
-+                    self.loc =
-+                        ffi::X509_NAME_get_index_by_NID(self.name.as_ptr(), nid.as_raw(), self.loc);
-+                    if self.loc == -1 {
-+                        return None;
-+                    }
-+                }
-+                None => {
-+                    // Iterate over all `Nid`s
-+                    self.loc += 1;
-+                    if self.loc >= ffi::X509_NAME_entry_count(self.name.as_ptr()) {
-+                        return None;
-+                    }
-+                }
-+            }
-+
-+            let entry = ffi::X509_NAME_get_entry(self.name.as_ptr(), self.loc);
-+
-+            Some(X509NameEntryRef::from_const_ptr_opt(entry).expect("entry must not be null"))
-+        }
-+    }
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_NAME_ENTRY;
-+    fn drop = ffi::X509_NAME_ENTRY_free;
-+
-+    /// A name entry associated with a `X509Name`.
-+    pub struct X509NameEntry;
-+    /// Reference to `X509NameEntry`.
-+    pub struct X509NameEntryRef;
-+}
-+
-+impl X509NameEntryRef {
-+    /// Returns the field value of an `X509NameEntry`.
-+    ///
-+    /// This corresponds to [`X509_NAME_ENTRY_get_data`].
-+    ///
-+    /// [`X509_NAME_ENTRY_get_data`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_ENTRY_get_data.html
-+    pub fn data(&self) -> &Asn1StringRef {
-+        unsafe {
-+            let data = ffi::X509_NAME_ENTRY_get_data(self.as_ptr());
-+            Asn1StringRef::from_ptr(data)
-+        }
-+    }
-+
-+    /// Returns the `Asn1Object` value of an `X509NameEntry`.
-+    /// This is useful for finding out about the actual `Nid` when iterating over all `X509NameEntries`.
-+    ///
-+    /// This corresponds to [`X509_NAME_ENTRY_get_object`].
-+    ///
-+    /// [`X509_NAME_ENTRY_get_object`]: https://www.openssl.org/docs/manmaster/crypto/X509_NAME_ENTRY_get_object.html
-+    pub fn object(&self) -> &Asn1ObjectRef {
-+        unsafe {
-+            let object = ffi::X509_NAME_ENTRY_get_object(self.as_ptr());
-+            Asn1ObjectRef::from_ptr(object)
-+        }
-+    }
-+}
-+
-+impl fmt::Debug for X509NameEntryRef {
-+    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
-+        formatter.write_fmt(format_args!("{:?} = {:?}", self.object(), self.data()))
-+    }
-+}
-+
-+/// A builder used to construct an `X509Req`.
-+pub struct X509ReqBuilder(X509Req);
-+
-+impl X509ReqBuilder {
-+    /// Returns a builder for a certificate request.
-+    ///
-+    /// This corresponds to [`X509_REQ_new`].
-+    ///
-+    ///[`X509_REQ_new`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_new.html
-+    pub fn new() -> Result<X509ReqBuilder, ErrorStack> {
-+        unsafe {
-+            ffi::init();
-+            cvt_p(ffi::X509_REQ_new()).map(|p| X509ReqBuilder(X509Req(p)))
-+        }
-+    }
-+
-+    /// Set the numerical value of the version field.
-+    ///
-+    /// This corresponds to [`X509_REQ_set_version`].
-+    ///
-+    ///[`X509_REQ_set_version`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_set_version.html
-+    #[allow(clippy::useless_conversion)]
-+    pub fn set_version(&mut self, version: i32) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_REQ_set_version(
-+                self.0.as_ptr(),
-+                version as c_long,
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Set the issuer name.
-+    ///
-+    /// This corresponds to [`X509_REQ_set_subject_name`].
-+    ///
-+    /// [`X509_REQ_set_subject_name`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_set_subject_name.html
-+    pub fn set_subject_name(&mut self, subject_name: &X509NameRef) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_REQ_set_subject_name(
-+                self.0.as_ptr(),
-+                subject_name.as_ptr(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Set the public key.
-+    ///
-+    /// This corresponds to [`X509_REQ_set_pubkey`].
-+    ///
-+    /// [`X509_REQ_set_pubkey`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_set_pubkey.html
-+    pub fn set_pubkey<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
-+    where
-+        T: HasPublic,
-+    {
-+        unsafe { cvt(ffi::X509_REQ_set_pubkey(self.0.as_ptr(), key.as_ptr())).map(|_| ()) }
-+    }
-+
-+    /// Return an `X509v3Context`. This context object can be used to construct
-+    /// certain `X509` extensions.
-+    pub fn x509v3_context<'a>(&'a self, conf: Option<&'a ConfRef>) -> X509v3Context<'a> {
-+        unsafe {
-+            let mut ctx = mem::zeroed();
-+
-+            ffi::X509V3_set_ctx(
-+                &mut ctx,
-+                ptr::null_mut(),
-+                ptr::null_mut(),
-+                self.0.as_ptr(),
-+                ptr::null_mut(),
-+                0,
-+            );
-+
-+            // nodb case taken care of since we zeroed ctx above
-+            if let Some(conf) = conf {
-+                ffi::X509V3_set_nconf(&mut ctx, conf.as_ptr());
-+            }
-+
-+            X509v3Context(ctx, PhantomData)
-+        }
-+    }
-+
-+    /// Permits any number of extension fields to be added to the certificate.
-+    pub fn add_extensions(
-+        &mut self,
-+        extensions: &StackRef<X509Extension>,
-+    ) -> Result<(), ErrorStack> {
-+        unsafe {
-+            cvt(ffi::X509_REQ_add_extensions(
-+                self.0.as_ptr(),
-+                extensions.as_ptr(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Sign the request using a private key.
-+    ///
-+    /// This corresponds to [`X509_REQ_sign`].
-+    ///
-+    /// [`X509_REQ_sign`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_sign.html
-+    pub fn sign<T>(&mut self, key: &PKeyRef<T>, hash: MessageDigest) -> Result<(), ErrorStack>
-+    where
-+        T: HasPrivate,
-+    {
-+        unsafe {
-+            cvt(ffi::X509_REQ_sign(
-+                self.0.as_ptr(),
-+                key.as_ptr(),
-+                hash.as_ptr(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Sign the request using a private key without a digest.
-+    ///
-+    /// This is the only way to sign with Ed25519 keys as BoringSSL doesn't support the null
-+    /// message digest.
-+    ///
-+    /// This corresponds to [`X509_REQ_sign`].
-+    ///
-+    /// [`X509_REQ_sign`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_REQ_sign.html
-+    #[cfg(boringssl)]
-+    pub fn sign_without_digest<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
-+    where
-+        T: HasPrivate,
-+    {
-+        unsafe {
-+            cvt(ffi::X509_REQ_sign(
-+                self.0.as_ptr(),
-+                key.as_ptr(),
-+                ptr::null(),
-+            ))
-+            .map(|_| ())
-+        }
-+    }
-+
-+    /// Returns the `X509Req`.
-+    pub fn build(self) -> X509Req {
-+        self.0
-+    }
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_REQ;
-+    fn drop = ffi::X509_REQ_free;
-+
-+    /// An `X509` certificate request.
-+    pub struct X509Req;
-+    /// Reference to `X509Req`.
-+    pub struct X509ReqRef;
-+}
-+
-+impl X509Req {
-+    /// A builder for `X509Req`.
-+    pub fn builder() -> Result<X509ReqBuilder, ErrorStack> {
-+        X509ReqBuilder::new()
-+    }
-+
-+    from_pem! {
-+        /// Deserializes a PEM-encoded PKCS#10 certificate request structure.
-+        ///
-+        /// The input should have a header of `-----BEGIN CERTIFICATE REQUEST-----`.
-+        ///
-+        /// This corresponds to [`PEM_read_bio_X509_REQ`].
-+        ///
-+        /// [`PEM_read_bio_X509_REQ`]: https://www.openssl.org/docs/manmaster/crypto/PEM_read_bio_X509_REQ.html
-+        from_pem,
-+        X509Req,
-+        ffi::PEM_read_bio_X509_REQ
-+    }
-+
-+    from_der! {
-+        /// Deserializes a DER-encoded PKCS#10 certificate request structure.
-+        ///
-+        /// This corresponds to [`d2i_X509_REQ`].
-+        ///
-+        /// [`d2i_X509_REQ`]: https://www.openssl.org/docs/manmaster/crypto/d2i_X509_REQ.html
-+        from_der,
-+        X509Req,
-+        ffi::d2i_X509_REQ
-+    }
-+}
-+
-+impl X509ReqRef {
-+    to_pem! {
-+        /// Serializes the certificate request to a PEM-encoded PKCS#10 structure.
-+        ///
-+        /// The output will have a header of `-----BEGIN CERTIFICATE REQUEST-----`.
-+        ///
-+        /// This corresponds to [`PEM_write_bio_X509_REQ`].
-+        ///
-+        /// [`PEM_write_bio_X509_REQ`]: https://www.openssl.org/docs/manmaster/crypto/PEM_write_bio_X509_REQ.html
-+        to_pem,
-+        ffi::PEM_write_bio_X509_REQ
-+    }
-+
-+    to_der! {
-+        /// Serializes the certificate request to a DER-encoded PKCS#10 structure.
-+        ///
-+        /// This corresponds to [`i2d_X509_REQ`].
-+        ///
-+        /// [`i2d_X509_REQ`]: https://www.openssl.org/docs/manmaster/crypto/i2d_X509_REQ.html
-+        to_der,
-+        ffi::i2d_X509_REQ
-+    }
-+
-+    to_pem! {
-+        /// Converts the request to human readable text.
-+        #[corresponds(X509_Req_print)]
-+        to_text,
-+        ffi::X509_REQ_print
-+    }
-+
-+    /// Returns the numerical value of the version field of the certificate request.
-+    ///
-+    /// This corresponds to [`X509_REQ_get_version`]
-+    ///
-+    /// [`X509_REQ_get_version`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_get_version.html
-+    pub fn version(&self) -> i32 {
-+        unsafe { X509_REQ_get_version(self.as_ptr()) as i32 }
-+    }
-+
-+    /// Returns the subject name of the certificate request.
-+    ///
-+    /// This corresponds to [`X509_REQ_get_subject_name`]
-+    ///
-+    /// [`X509_REQ_get_subject_name`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_get_subject_name.html
-+    pub fn subject_name(&self) -> &X509NameRef {
-+        unsafe {
-+            let name = X509_REQ_get_subject_name(self.as_ptr());
-+            X509NameRef::from_const_ptr_opt(name).expect("subject name must not be null")
-+        }
-+    }
-+
-+    /// Returns the public key of the certificate request.
-+    ///
-+    /// This corresponds to [`X509_REQ_get_pubkey"]
-+    ///
-+    /// [`X509_REQ_get_pubkey`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_get_pubkey.html
-+    pub fn public_key(&self) -> Result<PKey<Public>, ErrorStack> {
-+        unsafe {
-+            let key = cvt_p(ffi::X509_REQ_get_pubkey(self.as_ptr()))?;
-+            Ok(PKey::from_ptr(key))
-+        }
-+    }
-+
-+    /// Check if the certificate request is signed using the given public key.
-+    ///
-+    /// Returns `true` if verification succeeds.
-+    ///
-+    /// This corresponds to [`X509_REQ_verify"].
-+    ///
-+    /// [`X509_REQ_verify`]: https://www.openssl.org/docs/manmaster/crypto/X509_REQ_verify.html
-+    pub fn verify<T>(&self, key: &PKeyRef<T>) -> Result<bool, ErrorStack>
-+    where
-+        T: HasPublic,
-+    {
-+        unsafe { cvt_n(ffi::X509_REQ_verify(self.as_ptr(), key.as_ptr())).map(|n| n != 0) }
-+    }
-+
-+    /// Returns the extensions of the certificate request.
-+    ///
-+    /// This corresponds to [`X509_REQ_get_extensions"]
-+    pub fn extensions(&self) -> Result<Stack<X509Extension>, ErrorStack> {
-+        unsafe {
-+            let extensions = cvt_p(ffi::X509_REQ_get_extensions(self.as_ptr()))?;
-+            Ok(Stack::from_ptr(extensions))
-+        }
-+    }
-+}
-+
-+/// The result of peer certificate verification.
-+#[derive(Copy, Clone, PartialEq, Eq)]
-+pub struct X509VerifyResult(c_int);
-+
-+impl fmt::Debug for X509VerifyResult {
-+    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
-+        fmt.debug_struct("X509VerifyResult")
-+            .field("code", &self.0)
-+            .field("error", &self.error_string())
-+            .finish()
-+    }
-+}
-+
-+impl fmt::Display for X509VerifyResult {
-+    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
-+        fmt.write_str(self.error_string())
-+    }
-+}
-+
-+impl Error for X509VerifyResult {}
-+
-+impl X509VerifyResult {
-+    /// Creates an `X509VerifyResult` from a raw error number.
-+    ///
-+    /// # Safety
-+    ///
-+    /// Some methods on `X509VerifyResult` are not thread safe if the error
-+    /// number is invalid.
-+    pub unsafe fn from_raw(err: c_int) -> X509VerifyResult {
-+        X509VerifyResult(err)
-+    }
-+
-+    /// Return the integer representation of an `X509VerifyResult`.
-+    #[allow(clippy::trivially_copy_pass_by_ref)]
-+    pub fn as_raw(&self) -> c_int {
-+        self.0
-+    }
-+
-+    /// Return a human readable error string from the verification error.
-+    ///
-+    /// This corresponds to [`X509_verify_cert_error_string`].
-+    ///
-+    /// [`X509_verify_cert_error_string`]: https://www.openssl.org/docs/manmaster/crypto/X509_verify_cert_error_string.html
-+    #[allow(clippy::trivially_copy_pass_by_ref)]
-+    pub fn error_string(&self) -> &'static str {
-+        ffi::init();
-+
-+        unsafe {
-+            let s = ffi::X509_verify_cert_error_string(self.0 as c_long);
-+            str::from_utf8(CStr::from_ptr(s).to_bytes()).unwrap()
-+        }
-+    }
-+
-+    /// Successful peer certificate verification.
-+    pub const OK: X509VerifyResult = X509VerifyResult(ffi::X509_V_OK);
-+    /// Application verification failure.
-+    pub const APPLICATION_VERIFICATION: X509VerifyResult =
-+        X509VerifyResult(ffi::X509_V_ERR_APPLICATION_VERIFICATION);
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::GENERAL_NAME;
-+    fn drop = ffi::GENERAL_NAME_free;
-+
-+    /// An `X509` certificate alternative names.
-+    pub struct GeneralName;
-+    /// Reference to `GeneralName`.
-+    pub struct GeneralNameRef;
-+}
-+
-+impl GeneralNameRef {
-+    fn ia5_string(&self, ffi_type: c_int) -> Option<&str> {
-+        unsafe {
-+            if (*self.as_ptr()).type_ != ffi_type {
-+                return None;
-+            }
-+
-+            #[cfg(boringssl)]
-+            let d = (*self.as_ptr()).d.ptr;
-+            #[cfg(not(boringssl))]
-+            let d = (*self.as_ptr()).d;
-+
-+            let ptr = ASN1_STRING_get0_data(d as *mut _);
-+            let len = ffi::ASN1_STRING_length(d as *mut _);
-+
-+            let slice = slice::from_raw_parts(ptr as *const u8, len as usize);
-+            // IA5Strings are stated to be ASCII (specifically IA5). Hopefully
-+            // OpenSSL checks that when loading a certificate but if not we'll
-+            // use this instead of from_utf8_unchecked just in case.
-+            str::from_utf8(slice).ok()
-+        }
-+    }
-+
-+    /// Returns the contents of this `GeneralName` if it is an `rfc822Name`.
-+    pub fn email(&self) -> Option<&str> {
-+        self.ia5_string(ffi::GEN_EMAIL)
-+    }
-+
-+    /// Returns the contents of this `GeneralName` if it is a `dNSName`.
-+    pub fn dnsname(&self) -> Option<&str> {
-+        self.ia5_string(ffi::GEN_DNS)
-+    }
-+
-+    /// Returns the contents of this `GeneralName` if it is an `uniformResourceIdentifier`.
-+    pub fn uri(&self) -> Option<&str> {
-+        self.ia5_string(ffi::GEN_URI)
-+    }
-+
-+    /// Returns the contents of this `GeneralName` if it is an `iPAddress`.
-+    pub fn ipaddress(&self) -> Option<&[u8]> {
-+        unsafe {
-+            if (*self.as_ptr()).type_ != ffi::GEN_IPADD {
-+                return None;
-+            }
-+            #[cfg(boringssl)]
-+            let d: *const ffi::ASN1_STRING = std::mem::transmute((*self.as_ptr()).d);
-+            #[cfg(not(boringssl))]
-+            let d = (*self.as_ptr()).d;
-+
-+            let ptr = ASN1_STRING_get0_data(d as *mut _);
-+            let len = ffi::ASN1_STRING_length(d as *mut _);
-+
-+            Some(slice::from_raw_parts(ptr as *const u8, len as usize))
-+        }
-+    }
-+}
-+
-+impl fmt::Debug for GeneralNameRef {
-+    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
-+        if let Some(email) = self.email() {
-+            formatter.write_str(email)
-+        } else if let Some(dnsname) = self.dnsname() {
-+            formatter.write_str(dnsname)
-+        } else if let Some(uri) = self.uri() {
-+            formatter.write_str(uri)
-+        } else if let Some(ipaddress) = self.ipaddress() {
-+            let address = <[u8; 16]>::try_from(ipaddress)
-+                .map(IpAddr::from)
-+                .or_else(|_| <[u8; 4]>::try_from(ipaddress).map(IpAddr::from));
-+            match address {
-+                Ok(a) => fmt::Debug::fmt(&a, formatter),
-+                Err(_) => fmt::Debug::fmt(ipaddress, formatter),
-+            }
-+        } else {
-+            formatter.write_str("(empty)")
-+        }
-+    }
-+}
-+
-+impl Stackable for GeneralName {
-+    type StackType = ffi::stack_st_GENERAL_NAME;
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::ACCESS_DESCRIPTION;
-+    fn drop = ffi::ACCESS_DESCRIPTION_free;
-+
-+    /// `AccessDescription` of certificate authority information.
-+    pub struct AccessDescription;
-+    /// Reference to `AccessDescription`.
-+    pub struct AccessDescriptionRef;
-+}
-+
-+impl AccessDescriptionRef {
-+    /// Returns the access method OID.
-+    pub fn method(&self) -> &Asn1ObjectRef {
-+        unsafe { Asn1ObjectRef::from_ptr((*self.as_ptr()).method) }
-+    }
-+
-+    // Returns the access location.
-+    pub fn location(&self) -> &GeneralNameRef {
-+        unsafe { GeneralNameRef::from_ptr((*self.as_ptr()).location) }
-+    }
-+}
-+
-+impl Stackable for AccessDescription {
-+    type StackType = ffi::stack_st_ACCESS_DESCRIPTION;
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_ALGOR;
-+    fn drop = ffi::X509_ALGOR_free;
-+
-+    /// An `X509` certificate signature algorithm.
-+    pub struct X509Algorithm;
-+    /// Reference to `X509Algorithm`.
-+    pub struct X509AlgorithmRef;
-+}
-+
-+impl X509AlgorithmRef {
-+    /// Returns the ASN.1 OID of this algorithm.
-+    pub fn object(&self) -> &Asn1ObjectRef {
-+        unsafe {
-+            let mut oid = ptr::null();
-+            X509_ALGOR_get0(&mut oid, ptr::null_mut(), ptr::null_mut(), self.as_ptr());
-+            Asn1ObjectRef::from_const_ptr_opt(oid).expect("algorithm oid must not be null")
-+        }
-+    }
-+}
-+
-+foreign_type_and_impl_send_sync! {
-+    type CType = ffi::X509_OBJECT;
-+    fn drop = X509_OBJECT_free;
-+
-+    /// An `X509` or an X509 certificate revocation list.
-+    pub struct X509Object;
-+    /// Reference to `X509Object`
-+    pub struct X509ObjectRef;
-+}
-+
-+impl X509ObjectRef {
-+    pub fn x509(&self) -> Option<&X509Ref> {
-+        unsafe {
-+            let ptr = X509_OBJECT_get0_X509(self.as_ptr());
-+            X509Ref::from_const_ptr_opt(ptr)
-+        }
-+    }
-+}
-+
-+impl Stackable for X509Object {
-+    type StackType = ffi::stack_st_X509_OBJECT;
-+}
-+
-+cfg_if! {
-+    if #[cfg(any(boringssl, ossl110, libressl273))] {
-+        use ffi::{X509_getm_notAfter, X509_getm_notBefore, X509_up_ref, X509_get0_signature};
-+    } else {
-+        #[allow(bad_style)]
-+        unsafe fn X509_getm_notAfter(x: *mut ffi::X509) -> *mut ffi::ASN1_TIME {
-+            (*(*(*x).cert_info).validity).notAfter
-+        }
-+
-+        #[allow(bad_style)]
-+        unsafe fn X509_getm_notBefore(x: *mut ffi::X509) -> *mut ffi::ASN1_TIME {
-+            (*(*(*x).cert_info).validity).notBefore
-+        }
-+
-+        #[allow(bad_style)]
-+        unsafe fn X509_up_ref(x: *mut ffi::X509) {
-+            ffi::CRYPTO_add_lock(
-+                &mut (*x).references,
-+                1,
-+                ffi::CRYPTO_LOCK_X509,
-+                "mod.rs\0".as_ptr() as *const _,
-+                line!() as c_int,
-+            );
-+        }
-+
-+        #[allow(bad_style)]
-+        unsafe fn X509_get0_signature(
-+            psig: *mut *const ffi::ASN1_BIT_STRING,
-+            palg: *mut *const ffi::X509_ALGOR,
-+            x: *const ffi::X509,
-+        ) {
-+            if !psig.is_null() {
-+                *psig = (*x).signature;
-+            }
-+            if !palg.is_null() {
-+                *palg = (*x).sig_alg;
-+            }
-+        }
-+    }
-+}
-+
-+cfg_if! {
-+    if #[cfg(any(boringssl, ossl110, libressl350))] {
-+        use ffi::{
-+            X509_ALGOR_get0, ASN1_STRING_get0_data, X509_STORE_CTX_get0_chain, X509_set1_notAfter,
-+            X509_set1_notBefore, X509_REQ_get_version, X509_REQ_get_subject_name,
-+        };
-+    } else {
-+        use ffi::{
-+            ASN1_STRING_data as ASN1_STRING_get0_data,
-+            X509_STORE_CTX_get_chain as X509_STORE_CTX_get0_chain,
-+            X509_set_notAfter as X509_set1_notAfter,
-+            X509_set_notBefore as X509_set1_notBefore,
-+        };
-+
-+        #[allow(bad_style)]
-+        unsafe fn X509_REQ_get_version(x: *mut ffi::X509_REQ) -> ::libc::c_long {
-+            ffi::ASN1_INTEGER_get((*(*x).req_info).version)
-+        }
-+
-+        #[allow(bad_style)]
-+        unsafe fn X509_REQ_get_subject_name(x: *mut ffi::X509_REQ) -> *mut ::ffi::X509_NAME {
-+            (*(*x).req_info).subject
-+        }
-+
-+        #[allow(bad_style)]
-+        unsafe fn X509_ALGOR_get0(
-+            paobj: *mut *const ffi::ASN1_OBJECT,
-+            pptype: *mut c_int,
-+            pval: *mut *mut ::libc::c_void,
-+            alg: *const ffi::X509_ALGOR,
-+        ) {
-+            if !paobj.is_null() {
-+                *paobj = (*alg).algorithm;
-+            }
-+            assert!(pptype.is_null());
-+            assert!(pval.is_null());
-+        }
-+    }
-+}
-+
-+cfg_if! {
-+    if #[cfg(any(ossl110, boringssl, libressl270))] {
-+        use ffi::X509_OBJECT_get0_X509;
-+    } else {
-+        #[allow(bad_style)]
-+        unsafe fn X509_OBJECT_get0_X509(x: *mut ffi::X509_OBJECT) -> *mut ffi::X509 {
-+            if (*x).type_ == ffi::X509_LU_X509 {
-+                (*x).data.x509
-+            } else {
-+                ptr::null_mut()
-+            }
-+        }
-+    }
-+}
-+
-+cfg_if! {
-+    if #[cfg(any(ossl110, libressl350))] {
-+        use ffi::X509_OBJECT_free;
-+    } else if #[cfg(boringssl)] {
-+        use ffi::X509_OBJECT_free_contents as X509_OBJECT_free;
-+    } else {
-+        #[allow(bad_style)]
-+        unsafe fn X509_OBJECT_free(x: *mut ffi::X509_OBJECT) {
-+            ffi::X509_OBJECT_free_contents(x);
-+            ffi::CRYPTO_free(x as *mut libc::c_void);
-+        }
-+    }
-+}
-+
-+#[derive(Copy, Clone, PartialEq, Eq)]
-+pub struct X509PurposeId(c_int);
-+
-+impl X509PurposeId {
-+    pub const SSL_CLIENT: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_SSL_CLIENT);
-+    pub const SSL_SERVER: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_SSL_SERVER);
-+    pub const NS_SSL_SERVER: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_NS_SSL_SERVER);
-+    pub const SMIME_SIGN: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_SMIME_SIGN);
-+    pub const SMIME_ENCRYPT: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_SMIME_ENCRYPT);
-+    pub const CRL_SIGN: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_CRL_SIGN);
-+    pub const ANY: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_ANY);
-+    pub const OCSP_HELPER: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_OCSP_HELPER);
-+    pub const TIMESTAMP_SIGN: X509PurposeId = X509PurposeId(ffi::X509_PURPOSE_TIMESTAMP_SIGN);
-+
-+    /// Constructs an `X509PurposeId` from a raw OpenSSL value.
-+    pub fn from_raw(id: c_int) -> Self {
-+        X509PurposeId(id)
-+    }
-+
-+    /// Returns the raw OpenSSL value represented by this type.
-+    pub fn as_raw(&self) -> c_int {
-+        self.0
-+    }
-+}
-+
-+/// A reference to an [`X509_PURPOSE`].
-+pub struct X509PurposeRef(Opaque);
-+
-+/// Implements a wrapper type for the static `X509_PURPOSE` table in OpenSSL.
-+impl ForeignTypeRef for X509PurposeRef {
-+    type CType = ffi::X509_PURPOSE;
-+}
-+
-+impl X509PurposeRef {
-+    /// Get the internal table index of an X509_PURPOSE for a given short name. Valid short
-+    /// names include
-+    ///  - "sslclient",
-+    ///  - "sslserver",
-+    ///  - "nssslserver",
-+    ///  - "smimesign",
-+    ///  - "smimeencrypt",
-+    ///  - "crlsign",
-+    ///  - "any",
-+    ///  - "ocsphelper",
-+    ///  - "timestampsign"
-+    /// The index can be used with `X509PurposeRef::from_idx()` to get the purpose.
-+    #[allow(clippy::unnecessary_cast)]
-+    pub fn get_by_sname(sname: &str) -> Result<c_int, ErrorStack> {
-+        unsafe {
-+            let sname = CString::new(sname).unwrap();
-+            cfg_if! {
-+                if #[cfg(any(ossl110, libressl280))] {
-+                    let purpose = cvt_n(ffi::X509_PURPOSE_get_by_sname(sname.as_ptr() as *const _))?;
-+                } else {
-+                    let purpose = cvt_n(ffi::X509_PURPOSE_get_by_sname(sname.as_ptr() as *mut _))?;
-+                }
-+            }
-+            Ok(purpose)
-+        }
-+    }
-+    /// Get an `X509PurposeRef` for a given index value. The index can be obtained from e.g.
-+    /// `X509PurposeRef::get_by_sname()`.
-+    #[corresponds(X509_PURPOSE_get0)]
-+    pub fn from_idx(idx: c_int) -> Result<&'static X509PurposeRef, ErrorStack> {
-+        unsafe {
-+            let ptr = cvt_p(ffi::X509_PURPOSE_get0(idx))?;
-+            Ok(X509PurposeRef::from_ptr(ptr))
-+        }
-+    }
-+
-+    /// Get the purpose value from an X509Purpose structure. This value is one of
-+    /// - `X509_PURPOSE_SSL_CLIENT`
-+    /// - `X509_PURPOSE_SSL_SERVER`
-+    /// - `X509_PURPOSE_NS_SSL_SERVER`
-+    /// - `X509_PURPOSE_SMIME_SIGN`
-+    /// - `X509_PURPOSE_SMIME_ENCRYPT`
-+    /// - `X509_PURPOSE_CRL_SIGN`
-+    /// - `X509_PURPOSE_ANY`
-+    /// - `X509_PURPOSE_OCSP_HELPER`
-+    /// - `X509_PURPOSE_TIMESTAMP_SIGN`
-+    pub fn purpose(&self) -> X509PurposeId {
-+        unsafe {
-+            let x509_purpose: *mut ffi::X509_PURPOSE = self.as_ptr();
-+            X509PurposeId::from_raw((*x509_purpose).purpose)
-+        }
-+    }
-+}
--- 
-2.40.1.495.gc816e09b53d-goog
-
diff --git a/nearby/scripts/openssl-patches/0001-Apply-android-patches.patch b/nearby/scripts/openssl-patches/0001-Apply-android-patches.patch
new file mode 100644
index 0000000..0ee52b1
--- /dev/null
+++ b/nearby/scripts/openssl-patches/0001-Apply-android-patches.patch
@@ -0,0 +1,1017 @@
+From 5c9103f02cb56f0f04444b16dd67eeaa05429d47 Mon Sep 17 00:00:00 2001
+From: Nabil Wadih <nwadih@google.com>
+Date: Thu, 24 Aug 2023 15:51:26 -0700
+Subject: [PATCH] Apply android patches
+
+---
+ openssl/.cargo/config.toml |   2 +
+ openssl/src/asn1.rs        |   2 +-
+ openssl/src/bio.rs         |   6 +-
+ openssl/src/bn.rs          |   2 +-
+ openssl/src/cipher.rs      |   4 +
+ openssl/src/dh.rs          |   2 +-
+ openssl/src/dsa.rs         |   5 +-
+ openssl/src/ec.rs          |  20 ++++
+ openssl/src/ecdsa.rs       |   2 +-
+ openssl/src/encrypt.rs     |   4 +-
+ openssl/src/hash.rs        |   2 +-
+ openssl/src/hkdf.rs        |  89 +++++++++++++++
+ openssl/src/hmac.rs        | 217 +++++++++++++++++++++++++++++++++++++
+ openssl/src/lib.rs         |  12 ++
+ openssl/src/md_ctx.rs      |   2 +-
+ openssl/src/pkey.rs        |  22 ++--
+ openssl/src/pkey_ctx.rs    |  21 +++-
+ openssl/src/rsa.rs         |   2 +-
+ openssl/src/sign.rs        |  10 +-
+ openssl/src/symm.rs        |   7 +-
+ openssl/src/x509/mod.rs    |  52 +++++++--
+ 21 files changed, 439 insertions(+), 46 deletions(-)
+ create mode 100644 openssl/.cargo/config.toml
+ create mode 100644 openssl/src/hkdf.rs
+ create mode 100644 openssl/src/hmac.rs
+
+diff --git a/openssl/.cargo/config.toml b/openssl/.cargo/config.toml
+new file mode 100644
+index 00000000..e2b197d8
+--- /dev/null
++++ b/openssl/.cargo/config.toml
+@@ -0,0 +1,2 @@
++[patch.crates-io]
++bssl-ffi = { package = "bssl-sys", version = "0.1.0", path = "../../../boringssl/build/rust", optional=true }
+diff --git a/openssl/src/asn1.rs b/openssl/src/asn1.rs
+index b02f9ac4..939a1732 100644
+--- a/openssl/src/asn1.rs
++++ b/openssl/src/asn1.rs
+@@ -651,7 +651,7 @@ impl fmt::Debug for Asn1ObjectRef {
+ }
+ 
+ cfg_if! {
+-    if #[cfg(any(ossl110, libressl273))] {
++    if #[cfg(any(ossl110, libressl273, boringssl))] {
+         use ffi::ASN1_STRING_get0_data;
+     } else {
+         #[allow(bad_style)]
+diff --git a/openssl/src/bio.rs b/openssl/src/bio.rs
+index 6a72552a..03242188 100644
+--- a/openssl/src/bio.rs
++++ b/openssl/src/bio.rs
+@@ -4,7 +4,7 @@ use std::marker::PhantomData;
+ use std::ptr;
+ use std::slice;
+ 
+-use crate::cvt_p;
++use crate::{cvt_p, SignedLenType};
+ use crate::error::ErrorStack;
+ 
+ pub struct MemBioSlice<'a>(*mut ffi::BIO, PhantomData<&'a [u8]>);
+@@ -25,7 +25,7 @@ impl<'a> MemBioSlice<'a> {
+         let bio = unsafe {
+             cvt_p(BIO_new_mem_buf(
+                 buf.as_ptr() as *const _,
+-                buf.len() as c_int,
++                buf.len() as SignedLenType,
+             ))?
+         };
+ 
+@@ -78,7 +78,7 @@ cfg_if! {
+         use ffi::BIO_new_mem_buf;
+     } else {
+         #[allow(bad_style)]
+-        unsafe fn BIO_new_mem_buf(buf: *const ::libc::c_void, len: ::libc::c_int) -> *mut ffi::BIO {
++        unsafe fn BIO_new_mem_buf(buf: *const ::libc::c_void, len: SignedLenType) -> *mut ffi::BIO {
+             ffi::BIO_new_mem_buf(buf as *mut _, len)
+         }
+     }
+diff --git a/openssl/src/bn.rs b/openssl/src/bn.rs
+index 1cd00dd4..dbd7ae94 100644
+--- a/openssl/src/bn.rs
++++ b/openssl/src/bn.rs
+@@ -814,7 +814,7 @@ impl BigNumRef {
+     /// assert_eq!(&bn_vec, &[0, 0, 0x45, 0x43]);
+     /// ```
+     #[corresponds(BN_bn2binpad)]
+-    #[cfg(ossl110)]
++    #[cfg(any(boringssl, ossl110))]
+     pub fn to_vec_padded(&self, pad_to: i32) -> Result<Vec<u8>, ErrorStack> {
+         let mut v = Vec::with_capacity(pad_to as usize);
+         unsafe {
+diff --git a/openssl/src/cipher.rs b/openssl/src/cipher.rs
+index ab5f49d2..84a82654 100644
+--- a/openssl/src/cipher.rs
++++ b/openssl/src/cipher.rs
+@@ -208,6 +208,7 @@ impl Cipher {
+         unsafe { CipherRef::from_ptr(ffi::EVP_aes_192_cfb1() as *mut _) }
+     }
+ 
++    #[cfg(not(boringssl))]
+     pub fn aes_192_cfb128() -> &'static CipherRef {
+         unsafe { CipherRef::from_ptr(ffi::EVP_aes_192_cfb128() as *mut _) }
+     }
+@@ -253,6 +254,7 @@ impl Cipher {
+         unsafe { CipherRef::from_ptr(ffi::EVP_aes_256_cfb1() as *mut _) }
+     }
+ 
++    #[cfg(not(boringssl))]
+     pub fn aes_256_cfb128() -> &'static CipherRef {
+         unsafe { CipherRef::from_ptr(ffi::EVP_aes_256_cfb128() as *mut _) }
+     }
+@@ -282,11 +284,13 @@ impl Cipher {
+     }
+ 
+     #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
++    #[cfg(not(boringssl))]
+     pub fn bf_cbc() -> &'static CipherRef {
+         unsafe { CipherRef::from_ptr(ffi::EVP_bf_cbc() as *mut _) }
+     }
+ 
+     #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
++    #[cfg(not(boringssl))]
+     pub fn bf_ecb() -> &'static CipherRef {
+         unsafe { CipherRef::from_ptr(ffi::EVP_bf_ecb() as *mut _) }
+     }
+diff --git a/openssl/src/dh.rs b/openssl/src/dh.rs
+index 12170b99..e781543e 100644
+--- a/openssl/src/dh.rs
++++ b/openssl/src/dh.rs
+@@ -239,7 +239,7 @@ where
+ }
+ 
+ cfg_if! {
+-    if #[cfg(any(ossl110, libressl270))] {
++    if #[cfg(any(ossl110, libressl270, boringssl))] {
+         use ffi::{DH_set0_pqg, DH_get0_pqg, DH_get0_key, DH_set0_key};
+     } else {
+         #[allow(bad_style)]
+diff --git a/openssl/src/dsa.rs b/openssl/src/dsa.rs
+index 5f59ba8a..0aceeb55 100644
+--- a/openssl/src/dsa.rs
++++ b/openssl/src/dsa.rs
+@@ -7,6 +7,7 @@
+ 
+ use cfg_if::cfg_if;
+ use foreign_types::{ForeignType, ForeignTypeRef};
++#[cfg(not(boringssl))]
+ use libc::c_int;
+ use std::fmt;
+ use std::mem;
+@@ -283,7 +284,7 @@ impl<T> fmt::Debug for Dsa<T> {
+ }
+ 
+ cfg_if! {
+-    if #[cfg(any(ossl110, libressl273))] {
++    if #[cfg(any(ossl110, libressl273, boringssl))] {
+         use ffi::{DSA_get0_key, DSA_get0_pqg, DSA_set0_key, DSA_set0_pqg};
+     } else {
+         #[allow(bad_style)]
+@@ -462,7 +463,7 @@ impl DsaSigRef {
+ }
+ 
+ cfg_if! {
+-    if #[cfg(any(ossl110, libressl273))] {
++    if #[cfg(any(ossl110, libressl273, boringssl))] {
+         use ffi::{DSA_SIG_set0, DSA_SIG_get0};
+     } else {
+         #[allow(bad_style)]
+diff --git a/openssl/src/ec.rs b/openssl/src/ec.rs
+index 24b38322..20785428 100644
+--- a/openssl/src/ec.rs
++++ b/openssl/src/ec.rs
+@@ -954,6 +954,26 @@ impl EcKey<Private> {
+         EcKey<Private>,
+         ffi::d2i_ECPrivateKey
+     }
++
++    /// Decodes a DER-encoded elliptic curve private key structure for the specified curve.
++    #[corresponds(EC_KEY_parse_private_key)]
++    #[cfg(boringssl)]
++    pub fn private_key_from_der_for_group(
++        der: &[u8],
++        group: &EcGroupRef,
++    ) -> Result<EcKey<Private>, ErrorStack> {
++        unsafe {
++            let mut cbs = ffi::CBS {
++                data: der.as_ptr(),
++                len: der.len(),
++            };
++            cvt_p(ffi::EC_KEY_parse_private_key(
++                &mut cbs as *mut ffi::CBS,
++                group.as_ptr(),
++            ))
++            .map(|p| EcKey::from_ptr(p))
++        }
++    }
+ }
+ 
+ impl<T> Clone for EcKey<T> {
+diff --git a/openssl/src/ecdsa.rs b/openssl/src/ecdsa.rs
+index 0a960e7b..f3b27b39 100644
+--- a/openssl/src/ecdsa.rs
++++ b/openssl/src/ecdsa.rs
+@@ -110,7 +110,7 @@ impl EcdsaSigRef {
+ }
+ 
+ cfg_if! {
+-    if #[cfg(any(ossl110, libressl273))] {
++    if #[cfg(any(ossl110, libressl273, boringssl))] {
+         use ffi::{ECDSA_SIG_set0, ECDSA_SIG_get0};
+     } else {
+         #[allow(bad_style)]
+diff --git a/openssl/src/encrypt.rs b/openssl/src/encrypt.rs
+index 3cb10fcc..34a9eb8b 100644
+--- a/openssl/src/encrypt.rs
++++ b/openssl/src/encrypt.rs
+@@ -148,7 +148,7 @@ impl<'a> Encrypter<'a> {
+     /// This corresponds to [`EVP_PKEY_CTX_set_rsa_oaep_md`].
+     ///
+     /// [`EVP_PKEY_CTX_set_rsa_oaep_md`]: https://www.openssl.org/docs/manmaster/man3/EVP_PKEY_CTX_set_rsa_oaep_md.html
+-    #[cfg(any(ossl102, libressl310))]
++    #[cfg(any(ossl102, libressl310, boringssl))]
+     pub fn set_rsa_oaep_md(&mut self, md: MessageDigest) -> Result<(), ErrorStack> {
+         unsafe {
+             cvt(ffi::EVP_PKEY_CTX_set_rsa_oaep_md(
+@@ -352,7 +352,7 @@ impl<'a> Decrypter<'a> {
+     /// This corresponds to [`EVP_PKEY_CTX_set_rsa_oaep_md`].
+     ///
+     /// [`EVP_PKEY_CTX_set_rsa_oaep_md`]: https://www.openssl.org/docs/manmaster/man3/EVP_PKEY_CTX_set_rsa_oaep_md.html
+-    #[cfg(any(ossl102, libressl310))]
++    #[cfg(any(ossl102, libressl310, boringssl))]
+     pub fn set_rsa_oaep_md(&mut self, md: MessageDigest) -> Result<(), ErrorStack> {
+         unsafe {
+             cvt(ffi::EVP_PKEY_CTX_set_rsa_oaep_md(
+diff --git a/openssl/src/hash.rs b/openssl/src/hash.rs
+index 8e27505a..7f6fa89e 100644
+--- a/openssl/src/hash.rs
++++ b/openssl/src/hash.rs
+@@ -43,7 +43,7 @@ use crate::nid::Nid;
+ use crate::{cvt, cvt_p};
+ 
+ cfg_if! {
+-    if #[cfg(ossl110)] {
++    if #[cfg(any(ossl110, boringssl))] {
+         use ffi::{EVP_MD_CTX_free, EVP_MD_CTX_new};
+     } else {
+         use ffi::{EVP_MD_CTX_create as EVP_MD_CTX_new, EVP_MD_CTX_destroy as EVP_MD_CTX_free};
+diff --git a/openssl/src/hkdf.rs b/openssl/src/hkdf.rs
+new file mode 100644
+index 00000000..cc7e5b3a
+--- /dev/null
++++ b/openssl/src/hkdf.rs
+@@ -0,0 +1,89 @@
++use crate::cvt;
++use crate::error::ErrorStack;
++use crate::md::MdRef;
++use foreign_types::ForeignTypeRef;
++use openssl_macros::corresponds;
++
++/// Computes HKDF (as specified by RFC 5869).
++///
++/// HKDF is an Extract-and-Expand algorithm. It does not do any key stretching,
++/// and as such, is not suited to be used alone to generate a key from a
++/// password.
++#[corresponds(HKDF)]
++#[inline]
++pub fn hkdf(
++    out_key: &mut [u8],
++    md: &MdRef,
++    secret: &[u8],
++    salt: &[u8],
++    info: &[u8],
++) -> Result<(), ErrorStack> {
++    unsafe {
++        cvt(ffi::HKDF(
++            out_key.as_mut_ptr(),
++            out_key.len(),
++            md.as_ptr(),
++            secret.as_ptr(),
++            secret.len(),
++            salt.as_ptr(),
++            salt.len(),
++            info.as_ptr(),
++            info.len(),
++        ))?;
++    }
++
++    Ok(())
++}
++
++/// Computes a HKDF PRK (as specified by RFC 5869).
++///
++/// WARNING: This function orders the inputs differently from RFC 5869
++/// specification. Double-check which parameter is the secret/IKM and which is
++/// the salt when using.
++#[corresponds(HKDF_extract)]
++#[inline]
++pub fn hkdf_extract<'a>(
++    out_key: &'a mut [u8],
++    md: &MdRef,
++    secret: &[u8],
++    salt: &[u8],
++) -> Result<&'a [u8], ErrorStack> {
++    let mut out_len = out_key.len();
++    unsafe {
++        cvt(ffi::HKDF_extract(
++            out_key.as_mut_ptr(),
++            &mut out_len,
++            md.as_ptr(),
++            secret.as_ptr(),
++            secret.len(),
++            salt.as_ptr(),
++            salt.len(),
++        ))?;
++    }
++
++    Ok(&out_key[..out_len])
++}
++
++/// Computes a HKDF OKM (as specified by RFC 5869).
++#[corresponds(HKDF_expand)]
++#[inline]
++pub fn hkdf_expand(
++    out_key: &mut [u8],
++    md: &MdRef,
++    prk: &[u8],
++    info: &[u8],
++) -> Result<(), ErrorStack> {
++    unsafe {
++        cvt(ffi::HKDF_expand(
++            out_key.as_mut_ptr(),
++            out_key.len(),
++            md.as_ptr(),
++            prk.as_ptr(),
++            prk.len(),
++            info.as_ptr(),
++            info.len(),
++        ))?;
++    }
++
++    Ok(())
++}
+diff --git a/openssl/src/hmac.rs b/openssl/src/hmac.rs
+new file mode 100644
+index 00000000..465781e2
+--- /dev/null
++++ b/openssl/src/hmac.rs
+@@ -0,0 +1,217 @@
++use crate::error::ErrorStack;
++use crate::md::MdRef;
++use crate::{cvt, cvt_p};
++use ffi::HMAC_CTX;
++use foreign_types::ForeignTypeRef;
++use libc::{c_uint, c_void};
++use openssl_macros::corresponds;
++use std::convert::TryFrom;
++use std::ptr;
++
++/// Computes the HMAC as a one-shot operation.
++///
++/// Calculates the HMAC of data, using the given |key|
++/// and hash function |md|, and returns the result re-using the space from
++/// buffer |out|. On entry, |out| must contain at least |EVP_MD_size| bytes
++/// of space. The actual length of the result is used to resize the returned
++/// slice. An output size of |EVP_MAX_MD_SIZE| will always be large enough.
++/// It returns a resized |out| or ErrorStack on error.
++#[corresponds(HMAC)]
++#[inline]
++pub fn hmac<'a>(
++    md: &MdRef,
++    key: &[u8],
++    data: &[u8],
++    out: &'a mut [u8],
++) -> Result<&'a [u8], ErrorStack> {
++    assert!(out.len() >= md.size());
++    let mut out_len = c_uint::try_from(out.len()).unwrap();
++    unsafe {
++        cvt_p(ffi::HMAC(
++            md.as_ptr(),
++            key.as_ptr() as *const c_void,
++            key.len(),
++            data.as_ptr(),
++            data.len(),
++            out.as_mut_ptr(),
++            &mut out_len,
++        ))?;
++    }
++    Ok(&out[..out_len as usize])
++}
++
++/// A context object used to perform HMAC operations.
++///
++/// HMAC is a MAC (message authentication code), i.e. a keyed hash function used for message
++/// authentication, which is based on a hash function.
++///
++/// Note: Only available in boringssl. For openssl, use `PKey::hmac` instead.
++#[cfg(boringssl)]
++pub struct HmacCtx {
++    ctx: *mut HMAC_CTX,
++    output_size: usize,
++}
++
++#[cfg(boringssl)]
++impl HmacCtx {
++    /// Creates a new [HmacCtx] to use the hash function `md` and key `key`.
++    #[corresponds(HMAC_Init_ex)]
++    pub fn new(key: &[u8], md: &MdRef) -> Result<Self, ErrorStack> {
++        unsafe {
++            // Safety: If an error occurred, the resulting null from HMAC_CTX_new is converted into
++            // ErrorStack in the returned result by `cvt_p`.
++            let ctx = cvt_p(ffi::HMAC_CTX_new())?;
++            // Safety:
++            // - HMAC_Init_ex must be called with a context previously created with HMAC_CTX_new,
++            //   which is the line above.
++            // - HMAC_Init_ex may return an error if key is null but the md is different from
++            //   before. This is avoided here since key is guaranteed to be non-null.
++            cvt(ffi::HMAC_Init_ex(
++                ctx,
++                key.as_ptr() as *const c_void,
++                key.len(),
++                md.as_ptr(),
++                ptr::null_mut(),
++            ))?;
++            Ok(Self {
++                ctx,
++                output_size: md.size(),
++            })
++        }
++    }
++
++    /// `update` can be called repeatedly with chunks of the message `data` to be authenticated.
++    #[corresponds(HMAC_Update)]
++    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
++        unsafe {
++            // Safety: HMAC_Update returns 0 on error, and that is converted into ErrorStack in the
++            // returned result by `cvt`.
++            cvt(ffi::HMAC_Update(self.ctx, data.as_ptr(), data.len())).map(|_| ())
++        }
++    }
++
++    /// Finishes the HMAC process, and places the message authentication code in `output`.
++    /// The number of bytes written to `output` is returned.
++    ///
++    /// # Panics
++    ///
++    /// Panics if the `output` is smaller than the required size. The output size is indicated by
++    /// `md.size()` for the `Md` instance passed in [new]. An output size of |EVP_MAX_MD_SIZE| will
++    /// always be large enough.
++    #[corresponds(HMAC_Final)]
++    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack> {
++        assert!(output.len() >= self.output_size);
++        unsafe {
++            // Safety: The length assertion above makes sure that `HMAC_Final` will not write longer
++            // than the length of `output`.
++            let mut size: c_uint = 0;
++            cvt(ffi::HMAC_Final(
++                self.ctx,
++                output.as_mut_ptr(),
++                &mut size as *mut c_uint,
++            ))
++            .map(|_| size as usize)
++        }
++    }
++}
++
++impl Drop for HmacCtx {
++    #[corresponds(HMAC_CTX_free)]
++    fn drop(&mut self) {
++        unsafe {
++            ffi::HMAC_CTX_free(self.ctx);
++        }
++    }
++}
++
++#[cfg(test)]
++mod tests {
++    use super::*;
++    use crate::md::Md;
++
++    const SHA_256_DIGEST_SIZE: usize = 32;
++
++    #[test]
++    fn hmac_sha256_test() {
++        let expected_hmac = [
++            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
++            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
++            0x2e, 0x32, 0xcf, 0xf7,
++        ];
++        let mut out: [u8; SHA_256_DIGEST_SIZE] = [0; SHA_256_DIGEST_SIZE];
++        let key: [u8; 20] = [0x0b; 20];
++        let data = b"Hi There";
++        let hmac_result =
++            hmac(Md::sha256(), &key, data, &mut out).expect("Couldn't calculate sha256 hmac");
++        assert_eq!(&hmac_result, &expected_hmac);
++    }
++
++    #[test]
++    #[should_panic]
++    fn hmac_sha256_output_too_short() {
++        let mut out = vec![0_u8; 1];
++        let key: [u8; 20] = [0x0b; 20];
++        let data = b"Hi There";
++        hmac(Md::sha256(), &key, data, &mut out).expect("Couldn't calculate sha256 hmac");
++    }
++
++    #[test]
++    fn hmac_sha256_test_big_buffer() {
++        let expected_hmac = [
++            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
++            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
++            0x2e, 0x32, 0xcf, 0xf7,
++        ];
++        let mut out: [u8; 100] = [0; 100];
++        let key: [u8; 20] = [0x0b; 20];
++        let data = b"Hi There";
++        let hmac_result =
++            hmac(Md::sha256(), &key, data, &mut out).expect("Couldn't calculate sha256 hmac");
++        assert_eq!(hmac_result.len(), SHA_256_DIGEST_SIZE);
++        assert_eq!(&hmac_result, &expected_hmac);
++    }
++
++    #[test]
++    fn hmac_sha256_update_test() {
++        let expected_hmac = [
++            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
++            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
++            0x2e, 0x32, 0xcf, 0xf7,
++        ];
++        let mut out: [u8; SHA_256_DIGEST_SIZE] = [0; SHA_256_DIGEST_SIZE];
++        let key: [u8; 20] = [0x0b; 20];
++        let data = b"Hi There";
++        let mut hmac_ctx = HmacCtx::new(&key, Md::sha256()).unwrap();
++        hmac_ctx.update(data).unwrap();
++        let size = hmac_ctx.finalize(&mut out).unwrap();
++        assert_eq!(&out, &expected_hmac);
++        assert_eq!(size, SHA_256_DIGEST_SIZE);
++    }
++
++    #[test]
++    fn hmac_sha256_update_chunks_test() {
++        let expected_hmac = [
++            0xb0, 0x34, 0x4c, 0x61, 0xd8, 0xdb, 0x38, 0x53, 0x5c, 0xa8, 0xaf, 0xce, 0xaf, 0xb,
++            0xf1, 0x2b, 0x88, 0x1d, 0xc2, 0x0, 0xc9, 0x83, 0x3d, 0xa7, 0x26, 0xe9, 0x37, 0x6c,
++            0x2e, 0x32, 0xcf, 0xf7,
++        ];
++        let mut out: [u8; SHA_256_DIGEST_SIZE] = [0; SHA_256_DIGEST_SIZE];
++        let key: [u8; 20] = [0x0b; 20];
++        let mut hmac_ctx = HmacCtx::new(&key, Md::sha256()).unwrap();
++        hmac_ctx.update(b"Hi").unwrap();
++        hmac_ctx.update(b" There").unwrap();
++        let size = hmac_ctx.finalize(&mut out).unwrap();
++        assert_eq!(&out, &expected_hmac);
++        assert_eq!(size, SHA_256_DIGEST_SIZE);
++    }
++
++    #[test]
++    #[should_panic]
++    fn hmac_sha256_update_output_too_short() {
++        let mut out = vec![0_u8; 1];
++        let key: [u8; 20] = [0x0b; 20];
++        let mut hmac_ctx = HmacCtx::new(&key, Md::sha256()).unwrap();
++        hmac_ctx.update(b"Hi There").unwrap();
++        hmac_ctx.finalize(&mut out).unwrap();
++    }
++}
+diff --git a/openssl/src/lib.rs b/openssl/src/lib.rs
+index 891651ec..e8d07d8a 100644
+--- a/openssl/src/lib.rs
++++ b/openssl/src/lib.rs
+@@ -120,6 +120,9 @@
+ #![doc(html_root_url = "https://docs.rs/openssl/0.10")]
+ #![warn(rust_2018_idioms)]
+ 
++#[cfg(all(soong, boringssl))]
++extern crate bssl_ffi as ffi;
++
+ #[doc(inline)]
+ pub use ffi::init;
+ 
+@@ -155,6 +158,10 @@ pub mod ex_data;
+ #[cfg(not(any(libressl, ossl300)))]
+ pub mod fips;
+ pub mod hash;
++#[cfg(boringssl)]
++pub mod hkdf;
++#[cfg(boringssl)]
++pub mod hmac;
+ #[cfg(ossl300)]
+ pub mod lib_ctx;
+ pub mod md;
+@@ -189,6 +196,11 @@ type LenType = libc::size_t;
+ #[cfg(not(boringssl))]
+ type LenType = libc::c_int;
+ 
++#[cfg(boringssl)]
++type SignedLenType = libc::ssize_t;
++#[cfg(not(boringssl))]
++type SignedLenType = libc::c_int;
++
+ #[inline]
+ fn cvt_p<T>(r: *mut T) -> Result<*mut T, ErrorStack> {
+     if r.is_null() {
+diff --git a/openssl/src/md_ctx.rs b/openssl/src/md_ctx.rs
+index c4d3f06b..156f3c2f 100644
+--- a/openssl/src/md_ctx.rs
++++ b/openssl/src/md_ctx.rs
+@@ -93,7 +93,7 @@ use std::convert::TryFrom;
+ use std::ptr;
+ 
+ cfg_if! {
+-    if #[cfg(ossl110)] {
++    if #[cfg(any(ossl110, boringssl))] {
+         use ffi::{EVP_MD_CTX_free, EVP_MD_CTX_new};
+     } else {
+         use ffi::{EVP_MD_CTX_create as EVP_MD_CTX_new, EVP_MD_CTX_destroy as EVP_MD_CTX_free};
+diff --git a/openssl/src/pkey.rs b/openssl/src/pkey.rs
+index 2039e7e9..21ba7118 100644
+--- a/openssl/src/pkey.rs
++++ b/openssl/src/pkey.rs
+@@ -47,7 +47,7 @@ use crate::dh::Dh;
+ use crate::dsa::Dsa;
+ use crate::ec::EcKey;
+ use crate::error::ErrorStack;
+-#[cfg(ossl110)]
++#[cfg(any(boringssl, ossl110))]
+ use crate::pkey_ctx::PkeyCtx;
+ use crate::rsa::Rsa;
+ use crate::symm::Cipher;
+@@ -86,14 +86,14 @@ impl Id {
+     pub const DH: Id = Id(ffi::EVP_PKEY_DH);
+     pub const EC: Id = Id(ffi::EVP_PKEY_EC);
+ 
+-    #[cfg(ossl110)]
++    #[cfg(any(boringssl, ossl110))]
+     pub const HKDF: Id = Id(ffi::EVP_PKEY_HKDF);
+ 
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub const ED25519: Id = Id(ffi::EVP_PKEY_ED25519);
+     #[cfg(ossl111)]
+     pub const ED448: Id = Id(ffi::EVP_PKEY_ED448);
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub const X25519: Id = Id(ffi::EVP_PKEY_X25519);
+     #[cfg(ossl111)]
+     pub const X448: Id = Id(ffi::EVP_PKEY_X448);
+@@ -252,7 +252,7 @@ where
+     /// This function only works for algorithms that support raw public keys.
+     /// Currently this is: [`Id::X25519`], [`Id::ED25519`], [`Id::X448`] or [`Id::ED448`].
+     #[corresponds(EVP_PKEY_get_raw_public_key)]
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn raw_public_key(&self) -> Result<Vec<u8>, ErrorStack> {
+         unsafe {
+             let mut len = 0;
+@@ -303,7 +303,7 @@ where
+     /// This function only works for algorithms that support raw private keys.
+     /// Currently this is: [`Id::HMAC`], [`Id::X25519`], [`Id::ED25519`], [`Id::X448`] or [`Id::ED448`].
+     #[corresponds(EVP_PKEY_get_raw_private_key)]
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn raw_private_key(&self) -> Result<Vec<u8>, ErrorStack> {
+         unsafe {
+             let mut len = 0;
+@@ -484,7 +484,7 @@ impl PKey<Private> {
+         ctx.keygen()
+     }
+ 
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     fn generate_eddsa(id: Id) -> Result<PKey<Private>, ErrorStack> {
+         let mut ctx = PkeyCtx::new_id(id)?;
+         ctx.keygen_init()?;
+@@ -514,7 +514,7 @@ impl PKey<Private> {
+     /// assert_eq!(secret.len(), 32);
+     /// # Ok(()) }
+     /// ```
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn generate_x25519() -> Result<PKey<Private>, ErrorStack> {
+         PKey::generate_eddsa(Id::X25519)
+     }
+@@ -568,7 +568,7 @@ impl PKey<Private> {
+     /// assert_eq!(signature.len(), 64);
+     /// # Ok(()) }
+     /// ```
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn generate_ed25519() -> Result<PKey<Private>, ErrorStack> {
+         PKey::generate_eddsa(Id::ED25519)
+     }
+@@ -718,7 +718,7 @@ impl PKey<Private> {
+     ///
+     /// Algorithm types that support raw private keys are HMAC, X25519, ED25519, X448 or ED448
+     #[corresponds(EVP_PKEY_new_raw_private_key)]
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn private_key_from_raw_bytes(
+         bytes: &[u8],
+         key_type: Id,
+@@ -759,7 +759,7 @@ impl PKey<Public> {
+     ///
+     /// Algorithm types that support raw public keys are X25519, ED25519, X448 or ED448
+     #[corresponds(EVP_PKEY_new_raw_public_key)]
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn public_key_from_raw_bytes(
+         bytes: &[u8],
+         key_type: Id,
+diff --git a/openssl/src/pkey_ctx.rs b/openssl/src/pkey_ctx.rs
+index f79372fb..3d4203fa 100644
+--- a/openssl/src/pkey_ctx.rs
++++ b/openssl/src/pkey_ctx.rs
+@@ -470,7 +470,7 @@ impl<T> PkeyCtxRef<T> {
+     ///
+     /// Requires OpenSSL 1.1.0 or newer.
+     #[corresponds(EVP_PKEY_CTX_set_hkdf_md)]
+-    #[cfg(ossl110)]
++    #[cfg(any(ossl110, boringssl))]
+     #[inline]
+     pub fn set_hkdf_md(&mut self, digest: &MdRef) -> Result<(), ErrorStack> {
+         unsafe {
+@@ -503,10 +503,13 @@ impl<T> PkeyCtxRef<T> {
+     ///
+     /// Requires OpenSSL 1.1.0 or newer.
+     #[corresponds(EVP_PKEY_CTX_set1_hkdf_key)]
+-    #[cfg(ossl110)]
++    #[cfg(any(ossl110, boringssl))]
+     #[inline]
+     pub fn set_hkdf_key(&mut self, key: &[u8]) -> Result<(), ErrorStack> {
++        #[cfg(not(boringssl))]
+         let len = c_int::try_from(key.len()).unwrap();
++        #[cfg(boringssl)]
++        let len = key.len();
+ 
+         unsafe {
+             cvt(ffi::EVP_PKEY_CTX_set1_hkdf_key(
+@@ -523,10 +526,13 @@ impl<T> PkeyCtxRef<T> {
+     ///
+     /// Requires OpenSSL 1.1.0 or newer.
+     #[corresponds(EVP_PKEY_CTX_set1_hkdf_salt)]
+-    #[cfg(ossl110)]
++    #[cfg(any(ossl110, boringssl))]
+     #[inline]
+     pub fn set_hkdf_salt(&mut self, salt: &[u8]) -> Result<(), ErrorStack> {
++        #[cfg(not(boringssl))]
+         let len = c_int::try_from(salt.len()).unwrap();
++        #[cfg(boringssl)]
++        let len = salt.len();
+ 
+         unsafe {
+             cvt(ffi::EVP_PKEY_CTX_set1_hkdf_salt(
+@@ -543,10 +549,13 @@ impl<T> PkeyCtxRef<T> {
+     ///
+     /// Requires OpenSSL 1.1.0 or newer.
+     #[corresponds(EVP_PKEY_CTX_add1_hkdf_info)]
+-    #[cfg(ossl110)]
++    #[cfg(any(ossl110, boringssl))]
+     #[inline]
+     pub fn add_hkdf_info(&mut self, info: &[u8]) -> Result<(), ErrorStack> {
++        #[cfg(not(boringssl))]
+         let len = c_int::try_from(info.len()).unwrap();
++        #[cfg(boringssl)]
++        let len = info.len();
+ 
+         unsafe {
+             cvt(ffi::EVP_PKEY_CTX_add1_hkdf_info(
+@@ -604,7 +613,7 @@ mod test {
+     #[cfg(not(boringssl))]
+     use crate::cipher::Cipher;
+     use crate::ec::{EcGroup, EcKey};
+-    #[cfg(any(ossl102, libressl310))]
++    #[cfg(any(ossl102, libressl310, boringssl))]
+     use crate::md::Md;
+     use crate::nid::Nid;
+     use crate::pkey::PKey;
+@@ -689,7 +698,7 @@ mod test {
+     }
+ 
+     #[test]
+-    #[cfg(ossl110)]
++    #[cfg(any(ossl110, boringssl))]
+     fn hkdf() {
+         let mut ctx = PkeyCtx::new_id(Id::HKDF).unwrap();
+         ctx.derive_init().unwrap();
+diff --git a/openssl/src/rsa.rs b/openssl/src/rsa.rs
+index 68cf64b0..f155b12d 100644
+--- a/openssl/src/rsa.rs
++++ b/openssl/src/rsa.rs
+@@ -581,7 +581,7 @@ impl<T> fmt::Debug for Rsa<T> {
+ }
+ 
+ cfg_if! {
+-    if #[cfg(any(ossl110, libressl273))] {
++    if #[cfg(any(ossl110, libressl273, boringssl))] {
+         use ffi::{
+             RSA_get0_key, RSA_get0_factors, RSA_get0_crt_params, RSA_set0_key, RSA_set0_factors,
+             RSA_set0_crt_params,
+diff --git a/openssl/src/sign.rs b/openssl/src/sign.rs
+index b675825e..e5e80608 100644
+--- a/openssl/src/sign.rs
++++ b/openssl/src/sign.rs
+@@ -290,7 +290,7 @@ impl<'a> Signer<'a> {
+         self.len_intern()
+     }
+ 
+-    #[cfg(not(ossl111))]
++    #[cfg(not(any(boringssl, ossl111)))]
+     fn len_intern(&self) -> Result<usize, ErrorStack> {
+         unsafe {
+             let mut len = 0;
+@@ -303,7 +303,7 @@ impl<'a> Signer<'a> {
+         }
+     }
+ 
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     fn len_intern(&self) -> Result<usize, ErrorStack> {
+         unsafe {
+             let mut len = 0;
+@@ -360,7 +360,7 @@ impl<'a> Signer<'a> {
+     /// OpenSSL documentation at [`EVP_DigestSign`].
+     ///
+     /// [`EVP_DigestSign`]: https://www.openssl.org/docs/man1.1.1/man3/EVP_DigestSign.html
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn sign_oneshot(
+         &mut self,
+         sig_buf: &mut [u8],
+@@ -382,7 +382,7 @@ impl<'a> Signer<'a> {
+     /// Returns the signature.
+     ///
+     /// This is a simple convenience wrapper over `len` and `sign_oneshot`.
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn sign_oneshot_to_vec(&mut self, data_buf: &[u8]) -> Result<Vec<u8>, ErrorStack> {
+         let mut sig_buf = vec![0; self.len()?];
+         let len = self.sign_oneshot(&mut sig_buf, data_buf)?;
+@@ -596,7 +596,7 @@ impl<'a> Verifier<'a> {
+     /// OpenSSL documentation at [`EVP_DigestVerify`].
+     ///
+     /// [`EVP_DigestVerify`]: https://www.openssl.org/docs/man1.1.1/man3/EVP_DigestVerify.html
+-    #[cfg(ossl111)]
++    #[cfg(any(boringssl, ossl111))]
+     pub fn verify_oneshot(&mut self, signature: &[u8], buf: &[u8]) -> Result<bool, ErrorStack> {
+         unsafe {
+             let r = ffi::EVP_DigestVerify(
+diff --git a/openssl/src/symm.rs b/openssl/src/symm.rs
+index c75bbc0c..beff5fc2 100644
+--- a/openssl/src/symm.rs
++++ b/openssl/src/symm.rs
+@@ -119,6 +119,7 @@ impl Cipher {
+         unsafe { Cipher(ffi::EVP_aes_128_cfb1()) }
+     }
+ 
++    #[cfg(not(boringssl))]
+     pub fn aes_128_cfb128() -> Cipher {
+         unsafe { Cipher(ffi::EVP_aes_128_cfb128()) }
+     }
+@@ -164,6 +165,7 @@ impl Cipher {
+         unsafe { Cipher(ffi::EVP_aes_192_cfb1()) }
+     }
+ 
++    #[cfg(not(boringssl))]
+     pub fn aes_192_cfb128() -> Cipher {
+         unsafe { Cipher(ffi::EVP_aes_192_cfb128()) }
+     }
+@@ -214,6 +216,7 @@ impl Cipher {
+         unsafe { Cipher(ffi::EVP_aes_256_cfb1()) }
+     }
+ 
++    #[cfg(not(boringssl))]
+     pub fn aes_256_cfb128() -> Cipher {
+         unsafe { Cipher(ffi::EVP_aes_256_cfb128()) }
+     }
+@@ -242,12 +245,12 @@ impl Cipher {
+         unsafe { Cipher(ffi::EVP_aes_256_ocb()) }
+     }
+ 
+-    #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
++    #[cfg(not(any(boringssl, osslconf = "OPENSSL_NO_BF")))]
+     pub fn bf_cbc() -> Cipher {
+         unsafe { Cipher(ffi::EVP_bf_cbc()) }
+     }
+ 
+-    #[cfg(not(osslconf = "OPENSSL_NO_BF"))]
++    #[cfg(not(any(boringssl, osslconf = "OPENSSL_NO_BF")))]
+     pub fn bf_ecb() -> Cipher {
+         unsafe { Cipher(ffi::EVP_bf_ecb()) }
+     }
+diff --git a/openssl/src/x509/mod.rs b/openssl/src/x509/mod.rs
+index edd54aa8..a03a8aa6 100644
+--- a/openssl/src/x509/mod.rs
++++ b/openssl/src/x509/mod.rs
+@@ -353,6 +353,19 @@ impl X509Builder {
+         unsafe { cvt(ffi::X509_sign(self.0.as_ptr(), key.as_ptr(), hash.as_ptr())).map(|_| ()) }
+     }
+ 
++    /// Signs the certificate with a private key but without a digest.
++    ///
++    /// This is the only way to sign with Ed25519 keys as BoringSSL doesn't support the null
++    /// message digest.
++    #[cfg(boringssl)]
++    #[corresponds(X509_sign)]
++    pub fn sign_without_digest<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
++    where
++        T: HasPrivate,
++    {
++        unsafe { cvt(ffi::X509_sign(self.0.as_ptr(), key.as_ptr(), ptr::null())).map(|_| ()) }
++    }
++
+     /// Consumes the builder, returning the certificate.
+     pub fn build(self) -> X509 {
+         self.0
+@@ -880,13 +893,13 @@ impl X509NameBuilder {
+     pub fn append_entry_by_text(&mut self, field: &str, value: &str) -> Result<(), ErrorStack> {
+         unsafe {
+             let field = CString::new(field).unwrap();
+-            assert!(value.len() <= c_int::max_value() as usize);
++            assert!(value.len() <= isize::max_value() as usize);
+             cvt(ffi::X509_NAME_add_entry_by_txt(
+                 self.0.as_ptr(),
+                 field.as_ptr() as *mut _,
+                 ffi::MBSTRING_UTF8,
+                 value.as_ptr(),
+-                value.len() as c_int,
++                value.len() as isize,
+                 -1,
+                 0,
+             ))
+@@ -907,13 +920,13 @@ impl X509NameBuilder {
+     ) -> Result<(), ErrorStack> {
+         unsafe {
+             let field = CString::new(field).unwrap();
+-            assert!(value.len() <= c_int::max_value() as usize);
++            assert!(value.len() <= isize::max_value() as usize);
+             cvt(ffi::X509_NAME_add_entry_by_txt(
+                 self.0.as_ptr(),
+                 field.as_ptr() as *mut _,
+                 ty.as_raw(),
+                 value.as_ptr(),
+-                value.len() as c_int,
++                value.len() as isize,
+                 -1,
+                 0,
+             ))
+@@ -928,13 +941,13 @@ impl X509NameBuilder {
+     /// [`X509_NAME_add_entry_by_NID`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_NAME_add_entry_by_NID.html
+     pub fn append_entry_by_nid(&mut self, field: Nid, value: &str) -> Result<(), ErrorStack> {
+         unsafe {
+-            assert!(value.len() <= c_int::max_value() as usize);
++            assert!(value.len() <= isize::max_value() as usize);
+             cvt(ffi::X509_NAME_add_entry_by_NID(
+                 self.0.as_ptr(),
+                 field.as_raw(),
+                 ffi::MBSTRING_UTF8,
+                 value.as_ptr() as *mut _,
+-                value.len() as c_int,
++                value.len() as isize,
+                 -1,
+                 0,
+             ))
+@@ -954,13 +967,13 @@ impl X509NameBuilder {
+         ty: Asn1Type,
+     ) -> Result<(), ErrorStack> {
+         unsafe {
+-            assert!(value.len() <= c_int::max_value() as usize);
++            assert!(value.len() <= isize::max_value() as usize);
+             cvt(ffi::X509_NAME_add_entry_by_NID(
+                 self.0.as_ptr(),
+                 field.as_raw(),
+                 ty.as_raw(),
+                 value.as_ptr() as *mut _,
+-                value.len() as c_int,
++                value.len() as isize,
+                 -1,
+                 0,
+             ))
+@@ -1260,6 +1273,29 @@ impl X509ReqBuilder {
+         }
+     }
+ 
++    /// Sign the request using a private key without a digest.
++    ///
++    /// This is the only way to sign with Ed25519 keys as BoringSSL doesn't support the null
++    /// message digest.
++    ///
++    /// This corresponds to [`X509_REQ_sign`].
++    ///
++    /// [`X509_REQ_sign`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_REQ_sign.html
++    #[cfg(boringssl)]
++    pub fn sign_without_digest<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
++    where
++        T: HasPrivate,
++    {
++        unsafe {
++            cvt(ffi::X509_REQ_sign(
++                self.0.as_ptr(),
++                key.as_ptr(),
++                ptr::null(),
++            ))
++            .map(|_| ())
++        }
++    }
++
+     /// Returns the `X509Req`.
+     pub fn build(self) -> X509Req {
+         self.0
+-- 
+2.42.0.rc2.253.gd59a3bf2b4-goog
+
diff --git a/nearby/scripts/openssl-patches/0002-fix-boringssl-dsa-build-errors.patch b/nearby/scripts/openssl-patches/0002-fix-boringssl-dsa-build-errors.patch
deleted file mode 100644
index 030e46e..0000000
--- a/nearby/scripts/openssl-patches/0002-fix-boringssl-dsa-build-errors.patch
+++ /dev/null
@@ -1,25 +0,0 @@
-From 097eaa7166ad1f6298c41bc66e094a15a9a4e73e Mon Sep 17 00:00:00 2001
-From: Nabil Wadih <nwadih@google.com>
-Date: Tue, 6 Jun 2023 15:57:04 -0700
-Subject: [PATCH 2/2] fix boringssl dsa build errors
-
----
- openssl/src/dsa.rs | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/openssl/src/dsa.rs b/openssl/src/dsa.rs
-index c550f654..ffebdf8a 100644
---- a/openssl/src/dsa.rs
-+++ b/openssl/src/dsa.rs
-@@ -283,7 +283,7 @@ impl<T> fmt::Debug for Dsa<T> {
- }
- 
- cfg_if! {
--    if #[cfg(any(ossl110, libressl273))] {
-+    if #[cfg(any(ossl110, libressl273, boringssl))] {
-         use ffi::{DSA_get0_key, DSA_get0_pqg, DSA_set0_key, DSA_set0_pqg};
-     } else {
-         #[allow(bad_style)]
--- 
-2.41.0.162.gfafddb0af9-goog
-
diff --git a/nearby/src/crypto_ffi.rs b/nearby/src/crypto_ffi.rs
index 3118b8a..20d1d5d 100644
--- a/nearby/src/crypto_ffi.rs
+++ b/nearby/src/crypto_ffi.rs
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::support::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
-use crate::BuildBoringSslOptions;
 use anyhow::anyhow;
+use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
 use owo_colors::OwoColorize as _;
 use semver::{Version, VersionReq};
 use std::{
@@ -22,7 +21,15 @@
     path::{Path, PathBuf},
 };
 
-pub fn build_boringssl(root: &Path, options: &BuildBoringSslOptions) -> anyhow::Result<()> {
+use crate::CargoOptions;
+
+pub fn boringssl_check_everything(root: &Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
+    check_boringssl(root, cargo_options)?;
+    check_openssl(root, cargo_options)?;
+    Ok(())
+}
+
+pub fn build_boringssl(root: &Path) -> anyhow::Result<()> {
     let bindgen_version_req = VersionReq::parse(">=0.61.0")?;
     let bindgen_version = get_bindgen_version()?;
 
@@ -30,23 +37,16 @@
         return Err(anyhow!("Bindgen does not match expected version: {bindgen_version_req}"));
     }
 
-    let mut vendor_dir =
-        root.parent().ok_or_else(|| anyhow!("project root dir no parent dir"))?.to_path_buf();
-    vendor_dir.push("boringssl-build");
+    let vendor_dir = root
+        .parent()
+        .ok_or_else(|| anyhow!("project root dir no parent dir"))?
+        .join("boringssl-build");
     fs::create_dir_all(&vendor_dir)?;
 
-    let mut build_dir = clone_repo_if_needed(
-        &vendor_dir,
-        "boringssl",
-        "https://boringssl.googlesource.com/boringssl",
-    )?;
-
-    run_cmd_shell_with_color::<YellowStderr>(
-        &build_dir,
-        format!("git checkout {}", &options.commit_hash),
-    )?;
-
-    build_dir.push("build");
+    let build_dir = root
+        .parent()
+        .ok_or_else(|| anyhow!("project root dir no parent dir"))?
+        .join("third_party/boringssl/build");
     fs::create_dir_all(&build_dir)?;
 
     let target = run_cmd_shell_with_color::<YellowStderr>(&vendor_dir, "rustc -vV")?
@@ -70,19 +70,23 @@
     Ok(())
 }
 
-pub fn check_boringssl(root: &Path, options: &BuildBoringSslOptions) -> anyhow::Result<()> {
+pub fn check_boringssl(root: &Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
     log::info!("Checking boringssl");
 
-    build_boringssl(root, options)?;
+    build_boringssl(root)?;
 
-    let mut bssl_dir = root.to_path_buf();
-    bssl_dir.push("crypto/crypto_provider_boringssl");
+    let bssl_dir = root.join("crypto/crypto_provider_boringssl");
 
-    run_cmd_shell(&bssl_dir, "cargo check")?;
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
+
+    run_cmd_shell(&bssl_dir, format!("cargo check {locked_arg}"))?;
     run_cmd_shell(&bssl_dir, "cargo fmt --check")?;
     run_cmd_shell(&bssl_dir, "cargo clippy --all-targets")?;
-    run_cmd_shell(&bssl_dir, "cargo test -- --color=always")?;
+    run_cmd_shell(&bssl_dir, format!("cargo test {locked_arg} -- --color=always"))?;
     run_cmd_shell(&bssl_dir, "cargo doc --no-deps")?;
+
+    run_cmd_shell(root, "cargo test -p ukey2_connections -p ukey2_rs --no-default-features --features test_boringssl")?;
+
     Ok(())
 }
 
@@ -100,7 +104,7 @@
 
     run_cmd_shell_with_color::<YellowStderr>(
         &repo_dir,
-        "git checkout 11797d9ecb73e94b7f55a49274318abc9dc074d2",
+        "git checkout 7df56869c5e1e32369091ab106750d644d3aa0c4",
     )?;
     run_cmd_shell_with_color::<YellowStderr>(&repo_dir, "git branch -f BASE_COMMIT")?;
     run_cmd_shell_with_color::<YellowStderr>(
@@ -121,16 +125,20 @@
     Ok(())
 }
 
-pub fn check_openssl(root: &Path) -> anyhow::Result<()> {
+pub fn check_openssl(root: &Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
     log::info!("Checking rust openssl");
     prepare_patched_rust_openssl(root)?;
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
 
     // test the openssl crate with the boringssl feature
     run_cmd_shell(
         root,
-        concat!(
-            "cargo --config .cargo/config-boringssl.toml test -p crypto_provider_openssl ",
-            "--features=boringssl -- --color=always"
+        format!(
+            concat!(
+                "cargo --config .cargo/config-boringssl.toml test {locked_arg} -p crypto_provider_openssl ",
+                "--features=boringssl -- --color=always"
+            ),
+            locked_arg=locked_arg
         ),
     )?;
 
diff --git a/nearby/src/ffi.rs b/nearby/src/ffi.rs
index 44479be..01fe58a 100644
--- a/nearby/src/ffi.rs
+++ b/nearby/src/ffi.rs
@@ -12,56 +12,58 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
+use crate::CargoOptions;
+use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
 use std::{fs, path};
 
 // wrapper for checking all ffi related things
-pub fn check_everything(root: &path::Path) -> anyhow::Result<()> {
-    check_np_ffi(root)?;
-    check_ldt_ffi(root)?;
-    check_cmake_projects(root)?;
+pub fn check_everything(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
+    check_np_ffi_rust(root, cargo_options)?;
+    check_ldt_ffi_rust(root)?;
+    check_ldt_cmake(root, cargo_options)?;
+    check_np_ffi_cmake(root, cargo_options)?;
 
     Ok(())
 }
 
-pub fn check_np_ffi(root: &path::Path) -> anyhow::Result<()> {
+pub fn check_np_ffi_rust(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
     log::info!("Checking np_c_ffi cargo build");
-    let mut ffi_dir = root.to_path_buf();
-    ffi_dir.push("presence/np_c_ffi");
+    let ffi_dir = root.join("presence/np_c_ffi");
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
     for cargo_cmd in [
         "fmt --check",
-        // Default build, RustCrypto + no_std
-        "build --release",
-        "clippy --release",
+        // Default build, RustCrypto
+        format!("check {locked_arg} --quiet").as_str(),
+        // Build with BoringSSL for crypto
+        format!("check {locked_arg} --no-default-features --features=boringssl").as_str(),
+        "clippy",
+        "deny check",
+        "doc --quiet --no-deps",
     ] {
         run_cmd_shell(&ffi_dir, format!("cargo {}", cargo_cmd))?;
     }
     Ok(())
 }
 
-pub fn check_ldt_ffi(root: &path::Path) -> anyhow::Result<()> {
+pub fn check_ldt_ffi_rust(root: &path::Path) -> anyhow::Result<()> {
     log::info!("Checking LFT ffi cargo build");
-    let mut ffi_dir = root.to_path_buf();
-    ffi_dir.push("presence/ldt_np_adv_ffi");
+    let ffi_dir = root.to_path_buf().join("presence/ldt_np_adv_ffi");
 
     for cargo_cmd in [
         "fmt --check",
         // Default build, RustCrypto + no_std
-        "build --release",
+        "check --quiet",
         // Turn on std, still using RustCrypto
-        "build --features=std",
+        "check --quiet --features=std",
         // Turn off default features and try to build with std",
-        "build --no-default-features --features=std",
-        // Turn off RustCrypto and use openssl
-        "build --no-default-features --features=openssl",
+        "check --quiet --no-default-features --features=std",
         // Turn off RustCrypto and use boringssl
-        "--config .cargo/config-boringssl.toml build --no-default-features --features=boringssl",
-        "doc --no-deps",
+        "check --quiet --no-default-features --features=boringssl",
+        "doc --quiet --no-deps",
         "clippy --release",
         "clippy --features=std",
         "clippy --no-default-features --features=openssl",
         "clippy --no-default-features --features=std",
-        // TODO also clippy for boringssl?
         "deny check",
     ] {
         run_cmd_shell(&ffi_dir, format!("cargo {}", cargo_cmd))?;
@@ -70,44 +72,91 @@
     Ok(())
 }
 
-pub fn check_cmake_projects(root: &path::Path) -> anyhow::Result<()> {
-    log::info!("Checking CMake build and tests (for ffi c/c++ code)");
-    let mut build_dir = root.to_path_buf();
-    build_dir.push("presence/cmake-build");
+pub fn check_np_ffi_cmake(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
+    log::info!("Checking CMake build and tests for np ffi c/c++ code");
+    let build_dir = root.to_path_buf().join("presence/cmake-build");
     fs::create_dir_all(&build_dir)?;
 
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake .. -DENABLE_TESTS=true")?;
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build .")?;
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
 
-    // run the np_cpp_ffi unit tests
-    let mut np_cpp_tests_dir = build_dir.clone();
-    np_cpp_tests_dir.push("np_cpp_ffi/tests");
-    run_cmd_shell_with_color::<YellowStderr>(&np_cpp_tests_dir, "ctest")?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir,
+        "cmake -G Ninja -DENABLE_TESTS=true -DCMAKE_BUILD_TYPE=Release ..",
+    )?;
+
+    // verify sample and benchmarks build
+    let np_ffi_crate_dir = root.to_path_buf().join("presence/np_c_ffi");
+    run_cmd_shell(&np_ffi_crate_dir, format!("cargo build {locked_arg} --release"))?;
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target np_cpp_sample")?;
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target np_ffi_bench")?;
+
+    // Run tests with different crypto backends
+    let tests_dir = build_dir.to_path_buf().join("np_cpp_ffi/tests");
+    for build_config in [
+        // test with default build settings (rustcrypto)
+        format!("build {locked_arg} --quiet --release"),
+        // test with boringssl
+        format!("build {locked_arg} --quiet --no-default-features --features=boringssl"),
+    ] {
+        let _ = run_cmd_shell_with_color::<YellowStderr>(
+            &build_dir,
+            "rm np_cpp_ffi/tests/np_ffi_tests",
+        );
+        run_cmd_shell(&np_ffi_crate_dir, format!("cargo {}", build_config))?;
+        run_cmd_shell_with_color::<YellowStderr>(
+            &build_dir,
+            "cmake --build . --target np_ffi_tests",
+        )?;
+        run_cmd_shell_with_color::<YellowStderr>(&tests_dir, "ctest")?;
+    }
+
+    Ok(())
+}
+
+pub fn check_ldt_cmake(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
+    log::info!("Checking CMake build and tests for ldt c/c++ code");
+    let build_dir = root.to_path_buf().join("presence/cmake-build");
+    fs::create_dir_all(&build_dir)?;
+
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
+
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir,
+        "cmake -G Ninja -DENABLE_TESTS=true -DCMAKE_BUILD_TYPE=Release ..",
+    )?;
+
+    // verify sample and benchmarks build
+    let ldt_ffi_crate_dir = root.to_path_buf().join("presence/ldt_np_adv_ffi");
+    run_cmd_shell(&ldt_ffi_crate_dir, format!("cargo build {locked_arg} --release"))?;
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target ldt_c_sample")?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir,
+        "cmake --build . --target ldt_benchmarks",
+    )?;
 
     // Run the LDT ffi unit tests. These are rebuilt and tested against all of the different
     // Cargo build configurations based on the feature flags.
-    let mut ldt_tests_dir = build_dir.clone();
-    ldt_tests_dir.push("ldt_np_c_sample/tests");
-
-    let mut ldt_ffi_crate_dir = root.to_path_buf();
-    ldt_ffi_crate_dir.push("presence/ldt_np_adv_ffi");
-
+    let ldt_tests_dir = build_dir.to_path_buf().join("ldt_np_c_sample/tests");
     for build_config in [
         // test with default build settings (rustcrypto, no_std)
-        "build --release",
+        format!("build {locked_arg} --quiet --release"),
         // test with std and default features
-        "build --features std --release",
+        format!("build {locked_arg} --quiet --features std --release"),
         // test with boringssl crypto feature flag
-        "--config .cargo/config-boringssl.toml build --no-default-features --features boringssl --release",
-        // test with openssl feature flag
-        "build --no-default-features --features openssl --release",
+        format!("build {locked_arg} --quiet --no-default-features --features boringssl --release"),
         // test without defaults and std feature flag
-        "build --no-default-features --features std --release",
+        format!("build {locked_arg} --quiet --no-default-features --features std --release"),
     ] {
         run_cmd_shell(&ldt_ffi_crate_dir, format!("cargo {}", build_config))?;
         // Force detection of updated `ldt_np_adv_ffi` static lib
-        run_cmd_shell_with_color::<YellowStderr>(&build_dir, "rm -rf ldt_np_c_sample/tests/*")?;
-        run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build .")?;
+        let _ = run_cmd_shell_with_color::<YellowStderr>(
+            &build_dir,
+            "rm ldt_np_c_sample/tests/ldt_ffi_tests",
+        );
+        run_cmd_shell_with_color::<YellowStderr>(
+            &build_dir,
+            "cmake --build . --target ldt_ffi_tests",
+        )?;
         run_cmd_shell_with_color::<YellowStderr>(&ldt_tests_dir, "ctest")?;
     }
 
diff --git a/nearby/src/file_header/license.rs b/nearby/src/file_header/license.rs
deleted file mode 100644
index b463ad0..0000000
--- a/nearby/src/file_header/license.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Support for license-oriented usage of `file_header`.
-use super::*;
-use chrono::Datelike as _;
-
-/// The Apache 2 license for the current year and provided `copyright_holder`.
-pub fn apache_2(copyright_holder: &str) -> Header<impl HeaderChecker> {
-    Header::new(
-        asl2_checker(),
-        format!(
-            r#"Copyright {} {}
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License."#,
-            chrono::prelude::Utc::now().year(),
-            copyright_holder
-        ),
-    )
-}
-
-pub(crate) fn asl2_checker() -> impl HeaderChecker {
-    SingleLineChecker::new("Licensed under the Apache License, Version 2.0".to_string(), 10)
-}
diff --git a/nearby/src/file_header/mod.rs b/nearby/src/file_header/mod.rs
deleted file mode 100644
index 275a7d0..0000000
--- a/nearby/src/file_header/mod.rs
+++ /dev/null
@@ -1,559 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Tools for checking for, or adding, headers (e.g. licenses, etc) in files.
-
-use std::{
-    fs,
-    io::{self, BufRead as _, Write as _},
-    iter::FromIterator,
-    path, thread,
-};
-
-pub mod license;
-
-/// A file header to check for or add to files.
-#[derive(Clone)]
-pub struct Header<C: HeaderChecker> {
-    checker: C,
-    header: String,
-}
-
-impl<C: HeaderChecker> Header<C> {
-    /// Construct a new `Header` with the `checker` used to determine if the header is already
-    /// present, and the plain `header` text to add (without any applicable comment syntax, etc).
-    pub fn new(checker: C, header: String) -> Self {
-        Self { checker, header }
-    }
-
-    /// Return true if the file has the desired header, false otherwise.
-    pub fn header_present(&self, input: &mut impl io::Read) -> io::Result<bool> {
-        self.checker.check(input)
-    }
-
-    /// Add the header, with appropriate formatting for the type of file indicated by `p`'s
-    /// extension, if the header is not already present.
-    /// Returns true if the header was added.
-    pub fn add_header_if_missing(&self, p: &path::Path) -> Result<bool, AddHeaderError> {
-        let err_mapper = |e| AddHeaderError::IoError(p.to_path_buf(), e);
-        let contents = fs::read_to_string(p).map_err(err_mapper)?;
-
-        if self.header_present(&mut contents.as_bytes()).map_err(err_mapper)? {
-            return Ok(false);
-        }
-
-        let mut effective_header = header_delimiters(p)
-            .ok_or_else(|| AddHeaderError::UnknownExtension(p.to_path_buf()))
-            .map(|d| wrap_header(&self.header, d))?;
-
-        let mut after_header = contents.as_str();
-        // check for a magic first line
-        if let Some((first_line, rest)) = contents.split_once('\n') {
-            if MAGIC_FIRST_LINES.iter().any(|l| first_line.contains(l)) {
-                let mut first_line = first_line.to_string();
-                first_line.push('\n');
-                effective_header.insert_str(0, &first_line);
-                after_header = rest;
-            }
-        }
-
-        // write the license
-        let mut f =
-            fs::OpenOptions::new().write(true).truncate(true).open(p).map_err(err_mapper)?;
-        f.write_all(effective_header.as_bytes()).map_err(err_mapper)?;
-        // newline to separate the header from previous contents
-        f.write_all("\n".as_bytes()).map_err(err_mapper)?;
-        f.write_all(after_header.as_bytes()).map_err(err_mapper)?;
-
-        Ok(true)
-    }
-}
-
-/// Errors that can occur when adding a header
-#[derive(Debug, thiserror::Error)]
-pub enum AddHeaderError {
-    #[error("I/O error at {0:?}: {1}")]
-    IoError(path::PathBuf, io::Error),
-    #[error("Unknown file extension: {0:?}")]
-    UnknownExtension(path::PathBuf),
-}
-
-/// Checks for headers in files, like licenses or author attribution.
-pub trait HeaderChecker: Send + Clone {
-    /// Return true if the file has the desired header, false otherwise.
-    fn check(&self, file: &mut impl io::Read) -> io::Result<bool>;
-}
-
-/// Checks for a in the first several lines of each file.
-#[derive(Clone)]
-pub struct SingleLineChecker {
-    /// Pattern to do a substring match on in each of the first `max_lines` lines of the file
-    pattern: String,
-    /// Number of lines to search through
-    max_lines: usize,
-}
-
-impl SingleLineChecker {
-    /// Construct a `SingleLineChecker` that looks for `pattern` in the first `max_lines` of a file.
-    pub(crate) fn new(pattern: String, max_lines: usize) -> Self {
-        Self { pattern, max_lines }
-    }
-}
-
-impl HeaderChecker for SingleLineChecker {
-    fn check(&self, input: &mut impl io::Read) -> io::Result<bool> {
-        let mut reader = io::BufReader::new(input);
-        let mut lines_read = 0;
-        // reuse buffer to minimize allocation
-        let mut line = String::new();
-        // only read the first bit of the file
-        while lines_read < self.max_lines {
-            line.clear();
-            let bytes = reader.read_line(&mut line)?;
-            if bytes == 0 {
-                // EOF
-                return Ok(false);
-            }
-            lines_read += 1;
-
-            if line.contains(&self.pattern) {
-                return Ok(true);
-            }
-        }
-
-        Ok(false)
-    }
-}
-
-#[derive(Copy, Clone)]
-enum CheckStatus {
-    MisMatchedHeader,
-    BinaryFile,
-}
-
-#[derive(Clone)]
-struct FileResult {
-    path: path::PathBuf,
-    status: CheckStatus,
-}
-
-#[derive(Clone, Default)]
-pub struct FileResults {
-    pub mismatched_files: Vec<path::PathBuf>,
-    pub binary_files: Vec<path::PathBuf>,
-}
-
-impl FileResults {
-    pub fn has_failure(&self) -> bool {
-        !self.mismatched_files.is_empty() || !self.binary_files.is_empty()
-    }
-}
-
-impl FromIterator<FileResult> for FileResults {
-    fn from_iter<I>(iter: I) -> FileResults
-    where
-        I: IntoIterator<Item = FileResult>,
-    {
-        let mut results = FileResults::default();
-        for result in iter {
-            match result.status {
-                CheckStatus::MisMatchedHeader => results.mismatched_files.push(result.path),
-                CheckStatus::BinaryFile => results.binary_files.push(result.path),
-            }
-        }
-        results
-    }
-}
-
-/// Recursively check for `header` in every file in `root` that matches `path_predicate`.
-///
-/// Returns a [`FileResults`] object containing the paths without headers detected.
-pub fn check_headers_recursively(
-    root: &path::Path,
-    path_predicate: impl Fn(&path::Path) -> bool,
-    header: Header<impl HeaderChecker + 'static>,
-    num_threads: usize,
-) -> Result<FileResults, CheckHeadersRecursivelyError> {
-    let (path_tx, path_rx) = crossbeam::channel::unbounded::<path::PathBuf>();
-    let (result_tx, result_rx) = crossbeam::channel::unbounded();
-
-    // spawn a few threads to handle files in parallel
-    let handles = (0..num_threads)
-        .map(|_| {
-            let path_rx = path_rx.clone();
-            let result_tx = result_tx.clone();
-            let header = header.clone();
-            thread::spawn(move || {
-                for p in path_rx {
-                    match fs::File::open(&p).and_then(|mut f| header.header_present(&mut f)) {
-                        Ok(header_present) => {
-                            if header_present {
-                                // no op
-                            } else {
-                                let res =
-                                    FileResult { path: p, status: CheckStatus::MisMatchedHeader };
-                                result_tx.send(Ok(res)).unwrap();
-                            }
-                        }
-                        Err(e) if e.kind() == io::ErrorKind::InvalidData => {
-                            // Binary file - add to ignore in license.rs
-                            let res = FileResult { path: p, status: CheckStatus::BinaryFile };
-                            result_tx.send(Ok(res)).unwrap();
-                        }
-                        Err(e) => result_tx
-                            .send(Err(CheckHeadersRecursivelyError::IoError(p, e)))
-                            .unwrap(),
-                    }
-                }
-
-                // no more files
-            })
-        })
-        .collect::<Vec<thread::JoinHandle<()>>>();
-    // make sure result channel closes when threads complete
-    drop(result_tx);
-
-    find_files(root, path_predicate, path_tx)?;
-
-    let res: FileResults = result_rx.into_iter().collect::<Result<_, _>>()?;
-
-    for h in handles {
-        h.join().unwrap();
-    }
-
-    Ok(res)
-}
-
-/// Errors that can occur when checking for headers recursively
-#[derive(Debug, thiserror::Error)]
-pub enum CheckHeadersRecursivelyError {
-    #[error("I/O error at {0:?}: {1}")]
-    IoError(path::PathBuf, io::Error),
-    #[error("Walkdir error: {0}")]
-    WalkdirError(#[from] walkdir::Error),
-}
-
-/// Add the provided `header` to any file in `root` that matches `path_predicate` and that doesn't
-/// already have a header as determined by `checker`.
-/// Returns a list of paths that had headers added.
-pub fn add_headers_recursively(
-    root: &path::Path,
-    path_predicate: impl Fn(&path::Path) -> bool,
-    header: Header<impl HeaderChecker>,
-) -> Result<Vec<path::PathBuf>, AddHeadersRecursivelyError> {
-    // likely no need for threading since adding headers is only done occasionally
-    let (path_tx, path_rx) = crossbeam::channel::unbounded::<path::PathBuf>();
-    find_files(root, path_predicate, path_tx)?;
-
-    path_rx
-        .into_iter()
-        // keep the errors, or the ones with added headers
-        .filter_map(|p| {
-            match header.add_header_if_missing(&p).map_err(|e| match e {
-                AddHeaderError::IoError(p, e) => AddHeadersRecursivelyError::IoError(p, e),
-                AddHeaderError::UnknownExtension(e) => {
-                    AddHeadersRecursivelyError::UnknownExtension(e)
-                }
-            }) {
-                Ok(added) => {
-                    if added {
-                        Some(Ok(p))
-                    } else {
-                        None
-                    }
-                }
-                Err(e) => Some(Err(e)),
-            }
-        })
-        .collect::<Result<Vec<_>, _>>()
-}
-
-/// Errors that can occur when adding a header recursively
-#[derive(Debug, thiserror::Error)]
-pub enum AddHeadersRecursivelyError {
-    #[error("I/O error at {0:?}: {1}")]
-    IoError(path::PathBuf, io::Error),
-    #[error("Walkdir error: {0}")]
-    WalkdirError(#[from] walkdir::Error),
-    #[error("Unknown file extension: {0:?}")]
-    UnknownExtension(path::PathBuf),
-}
-
-/// Find all files starting from `root` that do not match the globs in `ignore`, publishing the
-/// resulting paths into `dest`.
-fn find_files(
-    root: &path::Path,
-    path_predicate: impl Fn(&path::Path) -> bool,
-    dest: crossbeam::channel::Sender<path::PathBuf>,
-) -> Result<(), walkdir::Error> {
-    for r in walkdir::WalkDir::new(root).into_iter() {
-        let entry = r?;
-        if entry.path().is_dir() || !path_predicate(entry.path()) {
-            continue;
-        }
-        dest.send(entry.into_path()).unwrap()
-    }
-
-    Ok(())
-}
-
-/// Prepare a header for inclusion in a particular file syntax by wrapping it with
-/// comment characters as per the provided `delim`.
-fn wrap_header(orig_header: &str, delim: HeaderDelimiters) -> String {
-    let mut out = String::new();
-
-    if !delim.first_line.is_empty() {
-        out.push_str(delim.first_line);
-        out.push('\n');
-    }
-
-    // assumes header uses \n
-    for line in orig_header.split('\n') {
-        out.push_str(delim.content_line_prefix);
-        out.push_str(line);
-        // Remove any trailing whitespaces (excluding newlines) from `content_line_prefix + line`.
-        // For example, if `content_line_prefix` is `// ` and `line` is empty, the resulting string
-        // should be truncated to `//`.
-        out.truncate(out.trim_end_matches([' ', '\t']).len());
-        out.push('\n');
-    }
-
-    if !delim.last_line.is_empty() {
-        out.push_str(delim.last_line);
-        out.push('\n');
-    }
-
-    out
-}
-
-/// Returns the header prefix line, content line prefix, and suffix line for the extension of the
-/// provided path, or `None` if the extension is not recognized.
-fn header_delimiters(p: &path::Path) -> Option<HeaderDelimiters> {
-    match p
-        .extension()
-        // if the extension isn't UTF-8, oh well
-        .and_then(|os_str| os_str.to_str())
-        .unwrap_or("")
-    {
-        "c" | "h" | "gv" | "java" | "scala" | "kt" | "kts" => Some(("/*", " * ", " */")),
-        "js" | "mjs" | "cjs" | "jsx" | "tsx" | "css" | "scss" | "sass" | "ts" => {
-            Some(("/**", " * ", " */"))
-        }
-        "cc" | "cpp" | "cs" | "go" | "hcl" | "hh" | "hpp" | "m" | "mm" | "proto" | "rs"
-        | "swift" | "dart" | "groovy" | "v" | "sv" => Some(("", "// ", "")),
-        "py" | "sh" | "yaml" | "yml" | "dockerfile" | "rb" | "gemfile" | "tcl" | "tf" | "bzl"
-        | "pl" | "pp" | "build" => Some(("", "# ", "")),
-        "el" | "lisp" => Some(("", ";; ", "")),
-        "erl" => Some(("", "% ", "")),
-        "hs" | "lua" | "sql" | "sdl" => Some(("", "-- ", "")),
-        "html" | "xml" | "vue" | "wxi" | "wxl" | "wxs" => Some(("<!--", " ", "-->")),
-        "php" => Some(("", "// ", "")),
-        "ml" | "mli" | "mll" | "mly" => Some(("(**", "   ", "*)")),
-        // also handle whole filenames if extensions didn't match
-        _ => match p.file_name().and_then(|os_str| os_str.to_str()).unwrap_or("") {
-            "Dockerfile" => Some(("", "# ", "")),
-            _ => None,
-        },
-    }
-    .map(|(first_line, content_line_prefix, last_line)| HeaderDelimiters {
-        first_line,
-        content_line_prefix,
-        last_line,
-    })
-}
-
-/// Delimiters to use around and inside a header for a particular file syntax.
-#[derive(Clone, Copy)]
-struct HeaderDelimiters {
-    /// Line to prepend before the header
-    first_line: &'static str,
-    /// Prefix before each line of the header itself
-    content_line_prefix: &'static str,
-    /// Line to append after the header
-    last_line: &'static str,
-}
-
-const MAGIC_FIRST_LINES: [&str; 8] = [
-    "#!",                       // shell script
-    "<?xml",                    // XML declaratioon
-    "<!doctype",                // HTML doctype
-    "# encoding:",              // Ruby encoding
-    "# frozen_string_literal:", // Ruby interpreter instruction
-    "<?php",                    // PHP opening tag
-    "# escape", // Dockerfile directive https://docs.docker.com/engine/reference/builder/#parser-directives
-    "# syntax", // Dockerfile directive https://docs.docker.com/engine/reference/builder/#parser-directives
-];
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn single_line_checker_finds_header_when_present() {
-        let input = r#"foo
-        some license
-        bar"#;
-
-        assert!(test_header().checker.check(&mut input.as_bytes()).unwrap());
-    }
-
-    #[test]
-    fn single_line_checker_doesnt_find_header_when_missing() {
-        let input = r#"foo
-        wrong license
-        bar"#;
-
-        assert!(!test_header().checker.check(&mut input.as_bytes()).unwrap());
-    }
-
-    #[test]
-    fn single_line_checker_throws_error_when_missing_and_file_is_non_utf8() {
-        let input = b"foo
-        \x00\xff
-        bar";
-
-        assert_eq!(
-            io::ErrorKind::InvalidData,
-            test_header().checker.check(&mut input.as_slice()).unwrap_err().kind()
-        );
-    }
-
-    #[test]
-    fn single_line_checker_doesnt_panic_when_file_is_non_utf8() {
-        let inputs: [&'static [u8]; 3] = [
-            b"foo
-            \x00\xff
-            bar",
-            b"foo
-            some license
-            \x00\xff
-            bar",
-            b"foo
-            \x00\xff
-            some license
-            bar",
-        ];
-
-        for mut input in inputs {
-            // Output is not defined for non-utf-8 files, but we should handle them with grace
-            let _ = test_header().checker.check(&mut input);
-        }
-    }
-
-    #[test]
-    fn adds_header_with_empty_delimiters() {
-        let file = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
-        fs::write(file.path(), r#"not a license"#).unwrap();
-
-        test_header().add_header_if_missing(file.path()).unwrap();
-
-        assert_eq!(
-            "// some license etc etc etc
-
-not a license",
-            fs::read_to_string(file.path()).unwrap()
-        );
-    }
-
-    #[test]
-    fn adds_header_with_nonempty_delimiters() {
-        let file = tempfile::Builder::new().suffix(".c").tempfile().unwrap();
-        fs::write(file.path(), r#"not a license"#).unwrap();
-
-        test_header().add_header_if_missing(file.path()).unwrap();
-
-        assert_eq!(
-            "/*
- * some license etc etc etc
- */
-
-not a license",
-            fs::read_to_string(file.path()).unwrap()
-        );
-    }
-
-    #[test]
-    fn adds_header_trim_trailing_whitespace() {
-        let file = tempfile::Builder::new().suffix(".c").tempfile().unwrap();
-        fs::write(file.path(), r#"not a license"#).unwrap();
-
-        test_header_with_blank_lines_and_trailing_whitespace()
-            .add_header_if_missing(file.path())
-            .unwrap();
-
-        assert_eq!(
-            "/*
- * some license
- * line with trailing whitespace.
- *
- * etc
- */
-
-not a license",
-            fs::read_to_string(file.path()).unwrap()
-        );
-    }
-
-    #[test]
-    fn doesnt_add_header_when_already_present() {
-        let file = tempfile::Builder::new().suffix(".rs").tempfile().unwrap();
-        let initial_content = r#"
-        // some license etc etc etc already present
-        not a license"#;
-        fs::write(file.path(), initial_content).unwrap();
-
-        test_header().add_header_if_missing(file.path()).unwrap();
-
-        assert_eq!(initial_content, fs::read_to_string(file.path()).unwrap());
-    }
-
-    #[test]
-    fn adds_header_after_magic_first_line() {
-        let file = tempfile::Builder::new().suffix(".xml").tempfile().unwrap();
-        fs::write(
-            file.path(),
-            r#"<?xml version="1.0" encoding="UTF-8"?>
-<root />
-"#,
-        )
-        .unwrap();
-
-        test_header().add_header_if_missing(file.path()).unwrap();
-
-        assert_eq!(
-            r#"<?xml version="1.0" encoding="UTF-8"?>
-<!--
- some license etc etc etc
--->
-
-<root />
-"#,
-            fs::read_to_string(file.path()).unwrap()
-        );
-    }
-
-    fn test_header() -> Header<SingleLineChecker> {
-        Header::new(
-            SingleLineChecker::new("some license".to_string(), 100),
-            r#"some license etc etc etc"#.to_string(),
-        )
-    }
-
-    fn test_header_with_blank_lines_and_trailing_whitespace() -> Header<SingleLineChecker> {
-        Header::new(
-            SingleLineChecker::new("some license".to_string(), 100),
-            "some license\nline with trailing whitespace.  \n\netc".to_string(),
-        )
-    }
-}
diff --git a/nearby/src/fuzzers.rs b/nearby/src/fuzzers.rs
index 6c84582..192d59c 100644
--- a/nearby/src/fuzzers.rs
+++ b/nearby/src/fuzzers.rs
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::{run_cmd_shell_with_color, YellowStderr};
+use cmd_runner::{run_cmd_shell_with_color, YellowStderr};
 use std::{fs, path};
 
 pub(crate) fn run_rust_fuzzers(root: &path::Path) -> anyhow::Result<()> {
@@ -70,8 +70,11 @@
         fs::remove_dir_all(&build_dir)?;
     }
     fs::create_dir_all(&build_dir)?;
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake ../.. -DENABLE_FUZZ=true")?;
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "make")?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir,
+        "cmake -G Ninja ../.. -DENABLE_FUZZ=true",
+    )?;
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build .")?;
     fs::remove_dir_all(&build_dir)?;
 
     Ok(())
diff --git a/nearby/src/jni.rs b/nearby/src/jni.rs
index 903ac6d..22a1da2 100644
--- a/nearby/src/jni.rs
+++ b/nearby/src/jni.rs
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::support::run_cmd_shell;
+use cmd_runner::run_cmd_shell;
 use std::path;
 
 // This has to happen after both boringssl has been built and prepare rust openssl patches has been run.
@@ -20,6 +20,11 @@
     for feature in ["opensslbssl", "boringssl"] {
         run_cmd_shell(root, format!("cargo --config .cargo/config-boringssl.toml build -p ldt_np_jni --no-default-features --features={}", feature))?;
     }
+    Ok(())
+}
 
+pub fn run_kotlin_tests(root: &path::Path) -> anyhow::Result<()> {
+    let kotlin_lib_path = root.to_path_buf().join("presence/ldt_np_jni/java/LdtNpJni");
+    run_cmd_shell(&kotlin_lib_path, "./gradlew :test")?;
     Ok(())
 }
diff --git a/nearby/src/license.rs b/nearby/src/license.rs
index 657a62e..e479526 100644
--- a/nearby/src/license.rs
+++ b/nearby/src/license.rs
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::file_header::{self, check_headers_recursively};
+use chrono::Datelike;
+use file_header::{check_headers_recursively, license::spdx::*};
 use std::path;
 
 pub(crate) fn check_license_headers(root: &path::Path) -> anyhow::Result<()> {
@@ -21,11 +22,14 @@
     let results = check_headers_recursively(
         root,
         |p| !ignore.is_match(p),
-        file_header::license::apache_2("Google LLC"),
+        APACHE_2_0.build_header(YearCopyrightOwnerValue::new(
+            u32::try_from(chrono::Utc::now().year())?,
+            "Google LLC".to_string(),
+        )),
         4,
     )?;
 
-    for path in results.mismatched_files.iter() {
+    for path in results.no_header_files.iter() {
         eprintln!("Header not present: {path:?}");
     }
 
@@ -48,7 +52,10 @@
     for p in file_header::add_headers_recursively(
         root,
         |p| !ignore.is_match(p),
-        file_header::license::apache_2("Google LLC"),
+        APACHE_2_0.build_header(YearCopyrightOwnerValue::new(
+            u32::try_from(chrono::Utc::now().year())?,
+            "Google LLC".to_string(),
+        )),
     )? {
         println!("Added header: {:?}", p);
     }
@@ -91,7 +98,7 @@
         "**/.DS_Store",
         "**/fuzz/corpus/**",
         "**/.*.swp",
-        "**/Session.vim",
+        "**/*.vim",
         "**/*.properties",
         "**/third_party/**",
         "**/*.png",
@@ -99,5 +106,7 @@
         "**/node_modules/**",
         "**/.angular/**",
         "**/.editorconfig",
+        "**/*.class",
+        "**/fuzz/artifacts/**",
     ]
 }
diff --git a/nearby/src/main.rs b/nearby/src/main.rs
index 4c1104f..eaf3ed2 100644
--- a/nearby/src/main.rs
+++ b/nearby/src/main.rs
@@ -15,17 +15,15 @@
 extern crate core;
 
 use clap::Parser as _;
+use cmd_runner::run_cmd_shell;
 use env_logger::Env;
 use std::{env, path};
-use support::*;
 
 mod crypto_ffi;
 mod ffi;
-mod file_header;
 mod fuzzers;
 mod jni;
 mod license;
-mod support;
 mod ukey2;
 
 fn main() -> anyhow::Result<()> {
@@ -37,28 +35,29 @@
     );
 
     match cli.subcommand {
-        Subcommand::CheckEverything { ref check_options, ref bssl_options } => {
-            check_everything(&root_dir, check_options, bssl_options)?
+        Subcommand::CheckEverything { ref check_options } => {
+            check_everything(&root_dir, check_options)?
         }
         Subcommand::CheckWorkspace(ref options) => check_workspace(&root_dir, options)?,
-        Subcommand::FfiCheckEverything => ffi::check_everything(&root_dir)?,
-        Subcommand::BuildBoringssl(ref bssl_options) => {
-            crypto_ffi::build_boringssl(&root_dir, bssl_options)?
+        Subcommand::FfiCheckEverything(ref options) => ffi::check_everything(&root_dir, options)?,
+        Subcommand::BoringsslCheckEverything(ref options) => {
+            crypto_ffi::boringssl_check_everything(&root_dir, options)?
         }
-        Subcommand::CheckBoringssl(ref bssl_options) => {
-            crypto_ffi::check_boringssl(&root_dir, bssl_options)?
-        }
+        Subcommand::BuildBoringssl => crypto_ffi::build_boringssl(&root_dir)?,
+        Subcommand::CheckBoringssl(ref options) => crypto_ffi::check_boringssl(&root_dir, options)?,
         Subcommand::PrepareRustOpenssl => crypto_ffi::prepare_patched_rust_openssl(&root_dir)?,
-        Subcommand::CheckOpenssl => crypto_ffi::check_openssl(&root_dir)?,
+        Subcommand::CheckOpenssl(ref options) => crypto_ffi::check_openssl(&root_dir, options)?,
         Subcommand::RunRustFuzzers => fuzzers::run_rust_fuzzers(&root_dir)?,
         Subcommand::BuildFfiFuzzers => fuzzers::build_ffi_fuzzers(&root_dir)?,
         Subcommand::CheckLicenseHeaders => license::check_license_headers(&root_dir)?,
         Subcommand::AddLicenseHeaders => license::add_license_headers(&root_dir)?,
-        Subcommand::CheckLdtFfi => ffi::check_ldt_ffi(&root_dir)?,
-        Subcommand::CheckUkey2Ffi => ukey2::check_ukey2_ffi(&root_dir)?,
+        Subcommand::CheckLdtFfi => ffi::check_ldt_ffi_rust(&root_dir)?,
+        Subcommand::CheckUkey2Ffi(ref options) => ukey2::check_ukey2_ffi(&root_dir, options)?,
         Subcommand::CheckLdtJni => jni::check_ldt_jni(&root_dir)?,
-        Subcommand::CheckNpFfi => ffi::check_np_ffi(&root_dir)?,
-        Subcommand::CheckCmakeProjects => ffi::check_cmake_projects(&root_dir)?,
+        Subcommand::CheckNpFfi(ref options) => ffi::check_np_ffi_rust(&root_dir, options)?,
+        Subcommand::CheckLdtCmake(ref options) => ffi::check_ldt_cmake(&root_dir, options)?,
+        Subcommand::CheckNpFfiCmake(ref options) => ffi::check_np_ffi_cmake(&root_dir, options)?,
+        Subcommand::RunKotlinTests => jni::run_kotlin_tests(&root_dir)?,
     }
 
     Ok(())
@@ -73,14 +72,21 @@
         // ensure formatting is correct (Check for it first because it is fast compared to running tests)
         fmt_command,
         // make sure everything compiles
-        "cargo check --workspace --all-targets",
+        "cargo check --workspace --all-targets --quiet",
         // run all the tests
         //TODO: re-enable the openssl tests, this was potentially failing due to UB code in the
         // upstream rust-openssl crate's handling of empty slices. This repros consistently when
         // using the rust-openssl crate backed by openssl-sys on Ubuntu 20.04.
         "cargo test --workspace --quiet --exclude crypto_provider_openssl -- --color=always",
+        // Test ukey2 builds with different crypto providers
+        "cargo test -p ukey2_connections -p ukey2_rs --no-default-features --features test_rustcrypto",
+        "cargo test -p ukey2_connections -p ukey2_rs --no-default-features --features test_openssl",
         // ensure the docs are valid (cross-references to other code, etc)
-        "RUSTDOCFLAGS='--deny warnings' cargo doc --workspace --no-deps",
+        concat!(
+            "RUSTDOCFLAGS='--deny warnings -Z unstable-options --enable-index-page --generate-link-to-definition' ",
+            "cargo +nightly doc --quiet --workspace --no-deps --document-private-items ",
+            "--target-dir target/dist_docs",
+        ),
         "cargo clippy --all-targets --workspace -- --deny warnings",
         "cargo deny --workspace check",
     ] {
@@ -89,18 +95,15 @@
 
     Ok(())
 }
-pub fn check_everything(
-    root: &path::Path,
-    check_options: &CheckOptions,
-    bssl_options: &BuildBoringSslOptions,
-) -> anyhow::Result<()> {
+pub fn check_everything(root: &path::Path, check_options: &CheckOptions) -> anyhow::Result<()> {
     license::check_license_headers(root)?;
     check_workspace(root, check_options)?;
-    crypto_ffi::check_boringssl(root, bssl_options)?;
-    crypto_ffi::check_openssl(root)?;
-    ffi::check_everything(root)?;
+    crypto_ffi::check_boringssl(root, &check_options.cargo_options)?;
+    crypto_ffi::check_openssl(root, &check_options.cargo_options)?;
+    ffi::check_everything(root, &check_options.cargo_options)?;
     jni::check_ldt_jni(root)?;
-    ukey2::check_ukey2_ffi(root)?;
+    jni::run_kotlin_tests(root)?;
+    ukey2::check_ukey2_ffi(root, &check_options.cargo_options)?;
     fuzzers::run_rust_fuzzers(root)?;
     fuzzers::build_ffi_fuzzers(root)?;
 
@@ -119,56 +122,58 @@
     CheckEverything {
         #[command(flatten)]
         check_options: CheckOptions,
-        #[command(flatten)]
-        bssl_options: BuildBoringSslOptions,
     },
     /// Checks everything included in the top level workspace
     CheckWorkspace(CheckOptions),
+    /// Checks everything related to the boringssl version (equivalent of running check-boringssl
+    /// + check-openssl)
+    BoringsslCheckEverything(CargoOptions),
     /// Clones boringssl and uses bindgen to generate the rust crate
-    BuildBoringssl(BuildBoringSslOptions),
+    BuildBoringssl,
     /// Run crypto provider tests using boringssl backend
-    CheckBoringssl(BuildBoringSslOptions),
+    CheckBoringssl(CargoOptions),
     /// Applies AOSP specific patches to the 3p `openssl` crate so that it can use a boringssl
     /// backend
     PrepareRustOpenssl,
     /// Run crypto provider tests using openssl crate with boringssl backend
-    CheckOpenssl,
+    CheckOpenssl(CargoOptions),
     /// Build and run pure Rust fuzzers for 10000 runs
     RunRustFuzzers,
     /// Build FFI fuzzers
     BuildFfiFuzzers,
     /// Builds and runs tests for all C/C++ projects. This is a combination of CheckNpFfi,
     /// CheckLdtFfi, and CheckCmakeBuildAndTests
-    FfiCheckEverything,
+    FfiCheckEverything(CargoOptions),
     /// Builds the crate checks the cbindgen generation of C/C++ bindings
-    CheckNpFfi,
+    CheckNpFfi(CargoOptions),
     /// Builds ldt_np_adv_ffi crate with all possible different sets of feature flags
     CheckLdtFfi,
     /// Checks the CMake build and runs all of the C/C++ tests
-    CheckCmakeProjects,
+    CheckLdtCmake(CargoOptions),
+    /// Checks the CMake build and runs all of the C/C++ tests
+    CheckNpFfiCmake(CargoOptions),
     /// Checks the workspace 3rd party crates and makes sure they have a valid license
     CheckLicenseHeaders,
     /// Generate new headers for any files that are missing them
     AddLicenseHeaders,
     /// Builds and runs tests for the UKEY2 FFI
-    CheckUkey2Ffi,
+    CheckUkey2Ffi(CargoOptions),
     /// Checks the build of ldt_jni wrapper with non default features, ie rust-openssl, and boringssl
     CheckLdtJni,
+    /// Runs the kotlin tests of the LDT Jni API
+    RunKotlinTests,
 }
 
 #[derive(clap::Args, Debug, Clone, Default)]
 pub struct CheckOptions {
     #[arg(long, help = "reformat files with cargo fmt")]
     reformat: bool,
+    #[command(flatten)]
+    cargo_options: CargoOptions,
 }
 
 #[derive(clap::Args, Debug, Clone, Default)]
-pub struct BuildBoringSslOptions {
-    #[arg(
-        long,
-        // the commit after this one causes failures in rust-openssl
-        default_value = "d995d82ad53133017e34b009e9c6912b2ef6aeb7",
-        help = "Commit hash to use when checking out boringssl"
-    )]
-    commit_hash: String,
+pub struct CargoOptions {
+    #[arg(long, help = "whether to run cargo with --locked")]
+    locked: bool,
 }
diff --git a/nearby/src/ukey2.rs b/nearby/src/ukey2.rs
index f0e1081..e548ab0 100644
--- a/nearby/src/ukey2.rs
+++ b/nearby/src/ukey2.rs
@@ -12,26 +12,34 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
+use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
 use std::{fs, path};
 
-pub(crate) fn check_ukey2_ffi(root: &path::Path) -> anyhow::Result<()> {
+use crate::CargoOptions;
+
+pub(crate) fn check_ukey2_ffi(
+    root: &path::Path,
+    cargo_options: &CargoOptions,
+) -> anyhow::Result<()> {
     log::info!("Checking Ukey2 ffi");
-    let mut ffi_dir = root.to_path_buf();
-    ffi_dir.push("connections/ukey2/ukey2_c_ffi");
+    let ffi_dir = root.join("connections/ukey2/ukey2_c_ffi");
+
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
 
     // Default build, RustCrypto
-    run_cmd_shell(&ffi_dir, "cargo build --release --lib")?;
+    run_cmd_shell(&ffi_dir, format!("cargo build {locked_arg} --quiet --release --lib"))?;
     // OpenSSL
-    run_cmd_shell(&ffi_dir, "cargo build --no-default-features --features=openssl")?;
+    run_cmd_shell(
+        &ffi_dir,
+        format!("cargo build {locked_arg} --quiet --no-default-features --features=openssl"),
+    )?;
 
-    run_cmd_shell(&ffi_dir, "cargo doc --no-deps")?;
+    run_cmd_shell(&ffi_dir, "cargo doc --quiet --no-deps")?;
     run_cmd_shell(&ffi_dir, "cargo clippy --no-default-features --features=openssl")?;
 
     run_cmd_shell(&ffi_dir, "cargo deny check")?;
 
-    let mut ffi_build_dir = ffi_dir.to_path_buf();
-    ffi_build_dir.push("cpp/build");
+    let ffi_build_dir = ffi_dir.join("cpp/build");
     fs::create_dir_all(&ffi_build_dir)?;
     run_cmd_shell_with_color::<YellowStderr>(&ffi_build_dir, "cmake ..")?;
     run_cmd_shell_with_color::<YellowStderr>(&ffi_build_dir, "cmake --build .")?;
diff --git a/nearby/presence/handle_map/Cargo.toml b/nearby/util/handle_map/Cargo.toml
similarity index 69%
rename from nearby/presence/handle_map/Cargo.toml
rename to nearby/util/handle_map/Cargo.toml
index 8f00bf7..ee9dbf3 100644
--- a/nearby/presence/handle_map/Cargo.toml
+++ b/nearby/util/handle_map/Cargo.toml
@@ -5,14 +5,12 @@
 publish.workspace = true
 
 [dependencies]
-hashbrown.workspace = true
-lock_api.workspace = true
-portable-atomic.workspace = true
-spin.workspace = true
+lock_adapter.workspace = true
 crypto_provider.workspace = true
 
 [dev-dependencies]
 criterion.workspace = true
+lazy_static.workspace = true
 
 [[bench]]
 name = "benches"
diff --git a/nearby/presence/handle_map/benches/benches.rs b/nearby/util/handle_map/benches/benches.rs
similarity index 100%
rename from nearby/presence/handle_map/benches/benches.rs
rename to nearby/util/handle_map/benches/benches.rs
diff --git a/nearby/util/handle_map/src/declare_handle_map.rs b/nearby/util/handle_map/src/declare_handle_map.rs
new file mode 100644
index 0000000..dbdc77f
--- /dev/null
+++ b/nearby/util/handle_map/src/declare_handle_map.rs
@@ -0,0 +1,161 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Implementation of the `declare_handle_map!` macro
+
+#[macro_export]
+/// ```ignore
+/// declare_handle_map! {
+///     mod $handle_module_name {
+///         #[dimensions = $map_dimension_provider]
+///         type $handle_type_name: HandleLike<Object = $wrapped_type>;
+///     }
+/// }
+/// ```
+///
+/// Declares a new public module with name `handle_module_name` which includes a new type
+/// `handle_type_name` which is `#[repr(C)]` and represents FFI-accessible handles
+/// to values of type `wrapped_type`.
+///
+/// Internal to the generated module, a new static `SingletonHandleMap` is created, where the
+/// maximum number of active handles and the number of shards are given by
+/// the dimensions returned by evaluation of the `map_dimension_provider` expression.
+///
+/// Note: `map_dimension_provider` will be evaluated within the defined module's scope,
+/// so you will likely need to use `super` to refer to definitions in the enclosing scope.
+///
+/// # Example
+/// The following code defines an FFI-safe type `StringHandle` which references
+/// the `String` data-type, and uses it to define a (contrived)
+/// function `sample` which will print "Hello World".
+///
+/// ```
+/// #[macro_use]///
+/// extern crate lazy_static;
+///
+/// use core::ops::Deref;
+/// use handle_map::{declare_handle_map, HandleMapDimensions, HandleLike};
+///
+/// fn get_string_handle_map_dimensions() -> HandleMapDimensions {
+///     HandleMapDimensions {
+///         num_shards: 8,
+///         max_active_handles: 100,
+///     }
+///  }
+///
+///  declare_handle_map! {
+///     mod string_handle {
+///         #[dimensions = super::get_string_handle_map_dimensions()]
+///         type StringHandle: HandleLike<Object = String>;
+///     }
+///  }
+///
+///  use string_handle::StringHandle;
+///
+///  fn main() {
+///         // Note: this method could panic if there are
+///         // more than 99 outstanding handles.
+///
+///         // Allocate a new string-handle pointing to the string "Hello"
+///         let handle = StringHandle::allocate(|| { "Hello".to_string() }).unwrap();
+///         {
+///             // Obtain a write-guard on the contents of our handle
+///             let mut handle_write_guard = handle.get_mut().unwrap();
+///             handle_write_guard.push_str(" World");
+///             // Write guard is auto-dropped at the end of this block.
+///         }
+///         {
+///             // Obtain a read-guard on the contents of our handle.
+///             // Note that we had to ensure that the write-guard was
+///             // dropped prior to doing this, or else execution
+///             // could potentially hang.
+///             let handle_read_guard = handle.get().unwrap();
+///             println!("{}", handle_read_guard.deref());
+///         }
+///         // Clean up the data behind the created handle
+///         handle.deallocate().unwrap();
+///  }
+///
+/// ```
+macro_rules! declare_handle_map {
+    (
+        mod $handle_module_name:ident {
+            #[dimensions = $map_dimension_provider:expr]
+            type $handle_type_name:ident: HandleLike<Object = $wrapped_type:ty>;
+        }
+     ) => {
+        #[doc = ::core::concat!(
+                    "Macro-generated (via `handle_map::declare_handle_map!`) module which",
+                    " defines the `", ::core::stringify!($handle_module_name), "::",
+                    ::core::stringify!($handle_type_name), "` FFI-transmissible handle type ",
+                    " which references values of type `", ::core::stringify!($wrapped_type), "`."
+                )]
+        pub mod $handle_module_name {
+
+            lazy_static! {
+                static ref GLOBAL_HANDLE_MAP: $crate::HandleMap<$wrapped_type> =
+                $crate::HandleMap::with_dimensions($map_dimension_provider);
+            }
+
+            #[doc = ::core::concat!(
+                        "A `#[repr(C)]` handle to a value of type `",
+                        ::core::stringify!($wrapped_type), "`."
+                    )]
+            #[repr(C)]
+            #[derive(Clone, Copy, PartialEq, Eq)]
+            pub struct $handle_type_name {
+                handle_id: u64,
+            }
+
+            impl $handle_type_name {
+                /// Cast the given raw Handle to this HandleLike
+                pub fn from_handle(handle: $crate::Handle) -> Self {
+                    Self { handle_id: handle.get_id() }
+                }
+
+                /// Get this HandleLike as a raw Handle.
+                pub fn get_as_handle(&self) -> $crate::Handle {
+                    $crate::Handle::from_id(self.handle_id)
+                }
+            }
+            impl $crate::HandleLike for $handle_type_name {
+                type Object = $wrapped_type;
+                fn allocate(
+                    initial_value_provider: impl FnOnce() -> $wrapped_type,
+                ) -> Result<Self, $crate::HandleMapFullError> {
+                    GLOBAL_HANDLE_MAP
+                        .allocate(initial_value_provider)
+                        .map(|derived_handle| Self { handle_id: derived_handle.get_id() })
+                }
+                fn get(
+                    &self,
+                ) -> Result<$crate::ObjectReadGuardImpl<$wrapped_type>, $crate::HandleNotPresentError>
+                {
+                    GLOBAL_HANDLE_MAP.get(self.get_as_handle())
+                }
+                fn get_mut(
+                    &self,
+                ) -> Result<
+                    $crate::ObjectReadWriteGuardImpl<$wrapped_type>,
+                    $crate::HandleNotPresentError,
+                > {
+                    GLOBAL_HANDLE_MAP.get_mut(self.get_as_handle())
+                }
+                fn deallocate(self) -> Result<$wrapped_type, $crate::HandleNotPresentError> {
+                    GLOBAL_HANDLE_MAP.deallocate(self.get_as_handle())
+                }
+            }
+        }
+    };
+}
diff --git a/nearby/util/handle_map/src/guard.rs b/nearby/util/handle_map/src/guard.rs
new file mode 100644
index 0000000..4a96bb5
--- /dev/null
+++ b/nearby/util/handle_map/src/guard.rs
@@ -0,0 +1,143 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::Handle;
+use core::ops::{Deref, DerefMut};
+use lock_adapter::std::RwMapping;
+use std::collections::HashMap;
+use std::marker::PhantomData;
+
+/// A RAII read lock guard for an object in a [`HandleMap`](crate::HandleMap)
+/// pointed-to by a given [`Handle`]. When this struct is
+/// dropped, the underlying read lock on the associated
+/// shard will be dropped.
+pub struct ObjectReadGuardImpl<'a, T: 'a> {
+    pub(crate) guard: lock_adapter::std::MappedRwLockReadGuard<
+        'a,
+        <Self as ObjectReadGuard>::Arg,
+        <Self as ObjectReadGuard>::Ret,
+        <Self as ObjectReadGuard>::Mapping,
+    >,
+}
+
+/// Trait implemented for an ObjectReadGuard which defines the associated types of the guard and a
+/// mapping for how the guard is retrieved from its parent object
+pub trait ObjectReadGuard: Deref<Target = Self::Ret> {
+    /// The mapping which defines how a guard is retrieved for `Self::Ret` from `Self::Arg`
+    type Mapping: RwMapping<Arg = Self::Arg, Ret = Self::Ret>;
+    /// The argument type input to the mapping functions
+    type Arg;
+    /// The Return type of the mapping functions
+    type Ret;
+}
+
+impl<'a, T> Deref for ObjectReadGuardImpl<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.guard.deref()
+    }
+}
+
+pub struct ObjectReadGuardMapping<'a, T> {
+    pub(crate) handle: Handle,
+    pub(crate) _marker: PhantomData<&'a T>,
+}
+
+impl<'a, T> RwMapping for ObjectReadGuardMapping<'a, T> {
+    type Arg = HashMap<Handle, T>;
+    type Ret = T;
+
+    fn map<'b>(&self, arg: &'b Self::Arg) -> &'b Self::Ret {
+        // We know that the entry exists, since we've locked the
+        // shard and already checked that it exists prior to
+        // handing out this new, mapped read-lock.
+        arg.get(&self.handle).unwrap()
+    }
+
+    fn map_mut<'b>(&self, arg: &'b mut Self::Arg) -> &'b mut Self::Ret {
+        // We know that the entry exists, since we've locked the
+        // shard and already checked that it exists prior to
+        // handing out this new, mapped read-lock.
+        arg.get_mut(&self.handle).unwrap()
+    }
+}
+
+impl<'a, T> ObjectReadGuard for ObjectReadGuardImpl<'a, T> {
+    type Mapping = ObjectReadGuardMapping<'a, T>;
+    type Arg = HashMap<Handle, T>;
+    type Ret = T;
+}
+
+/// A RAII read-write lock guard for an object in a [`HandleMap`](crate::HandleMap)
+/// pointed-to by a given [`Handle`]. When this struct is
+/// dropped, the underlying read-write lock on the associated
+/// shard will be dropped.
+pub struct ObjectReadWriteGuardImpl<'a, T: 'a> {
+    pub(crate) guard: lock_adapter::std::MappedRwLockWriteGuard<
+        'a,
+        <Self as ObjectReadWriteGuard>::Arg,
+        <Self as ObjectReadWriteGuard>::Ret,
+        <Self as ObjectReadWriteGuard>::Mapping,
+    >,
+}
+
+/// Trait implemented for an object read guard which defines the associated types of the guard and a
+/// mapping for how the guard is retrieved from its parent object
+pub trait ObjectReadWriteGuard: Deref<Target = Self::Ret> + DerefMut<Target = Self::Ret> {
+    /// The mapping which defines how a guard is retrieved for `Ret` from `Arg`
+    type Mapping: RwMapping<Arg = Self::Arg, Ret = Self::Ret>;
+    /// The argument type input to the mapping functions
+    type Arg;
+    /// The Return type of the mapping functions
+    type Ret;
+}
+
+pub struct ObjectReadWriteGuardMapping<'a, T> {
+    pub(crate) handle: Handle,
+    pub(crate) _marker: PhantomData<&'a T>,
+}
+
+impl<'a, T> Deref for ObjectReadWriteGuardImpl<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.guard.deref()
+    }
+}
+
+impl<'a, T> DerefMut for ObjectReadWriteGuardImpl<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.guard.deref_mut()
+    }
+}
+
+impl<'a, T> ObjectReadWriteGuard for ObjectReadWriteGuardImpl<'a, T> {
+    type Mapping = ObjectReadWriteGuardMapping<'a, T>;
+    type Arg = HashMap<Handle, T>;
+    type Ret = T;
+}
+
+impl<'a, T> RwMapping for ObjectReadWriteGuardMapping<'a, T> {
+    type Arg = HashMap<Handle, T>;
+    type Ret = T;
+
+    fn map<'b>(&self, arg: &'b Self::Arg) -> &'b Self::Ret {
+        arg.get(&self.handle).unwrap()
+    }
+
+    fn map_mut<'b>(&self, arg: &'b mut Self::Arg) -> &'b mut Self::Ret {
+        arg.get_mut(&self.handle).unwrap()
+    }
+}
diff --git a/nearby/util/handle_map/src/lib.rs b/nearby/util/handle_map/src/lib.rs
new file mode 100644
index 0000000..bb420e3
--- /dev/null
+++ b/nearby/util/handle_map/src/lib.rs
@@ -0,0 +1,242 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! A thread-safe implementation of a map for managing object handles,
+//! a safer alternative to raw pointers for FFI interop.
+
+#![forbid(unsafe_code)]
+#![deny(missing_docs)]
+
+use std::boxed::Box;
+use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
+use std::vec::Vec;
+
+pub mod declare_handle_map;
+mod guard;
+pub(crate) mod shard;
+
+#[cfg(test)]
+mod tests;
+
+pub use guard::{ObjectReadGuardImpl, ObjectReadWriteGuardImpl};
+
+use shard::{HandleMapShard, ShardAllocationError};
+
+/// An individual handle to be given out by a [`HandleMap`].
+/// This representation is untyped, and just a wrapper
+/// around a handle-id, in contrast to implementors of `HandleLike`.
+#[derive(Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Handle {
+    handle_id: u64,
+}
+
+impl From<&Handle> for Handle {
+    fn from(handle: &Handle) -> Self {
+        *handle
+    }
+}
+
+impl Handle {
+    /// Constructs a handle wrapping the given ID.
+    ///
+    /// No validity checks are done on the wrapped ID
+    /// to ensure that the given ID is active in
+    /// any specific handle-map, and the type
+    /// of the handle is not represented.
+    ///
+    /// As a result, this method is only useful for
+    /// allowing access to handles from an FFI layer.
+    pub fn from_id(handle_id: u64) -> Self {
+        Self { handle_id }
+    }
+
+    /// Gets the ID for this handle.
+    ///
+    /// Since the underlying handle is un-typed,`
+    /// this method is only suitable for
+    /// transmitting handles across an FFI layer.
+    pub fn get_id(&self) -> u64 {
+        self.handle_id
+    }
+
+    /// Derives the shard index from the handle id
+    fn get_shard_index(&self, num_shards: u8) -> usize {
+        (self.handle_id % (num_shards as u64)) as usize
+    }
+}
+
+/// Error raised when attempting to allocate into a full handle-map.
+#[derive(Debug)]
+pub struct HandleMapFullError;
+
+/// Error raised when the entry for a given [`Handle`] doesn't exist.
+#[derive(Debug)]
+pub struct HandleNotPresentError;
+
+/// FFI-transmissible structure expressing the dimensions
+/// (max # of allocatable slots, number of shards) of a handle-map
+/// to be used upon initialization.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct HandleMapDimensions {
+    /// The number of shards which are employed
+    /// by the associated handle-map.
+    pub num_shards: u8,
+    /// The maximum number of active handles which may be
+    /// stored within the associated handle-map.
+    pub max_active_handles: u32,
+}
+
+/// A thread-safe mapping from "handle"s [like pointers, but safer]
+/// to underlying structures, supporting allocations, reads, writes,
+/// and deallocations of objects behind handles.
+pub struct HandleMap<T: Send + Sync> {
+    /// The dimensions of this handle-map
+    dimensions: HandleMapDimensions,
+
+    /// The individually-lockable "shards" of the handle-map,
+    /// among which the keys will be roughly uniformly-distributed.
+    handle_map_shards: Box<[HandleMapShard<T>]>,
+
+    /// An atomically-incrementing counter which tracks the
+    /// next handle ID which allocations will attempt to use.
+    new_handle_id_counter: AtomicU64,
+
+    /// An atomic integer roughly tracking the number of
+    /// currently-outstanding allocated entries in this
+    /// handle-map among all [`HandleMapShard`]s.
+    outstanding_allocations_counter: AtomicU32,
+}
+
+impl<T: Send + Sync> HandleMap<T> {
+    /// Creates a new handle-map with the given `HandleMapDimensions`.
+    pub fn with_dimensions(dimensions: HandleMapDimensions) -> Self {
+        let mut handle_map_shards = Vec::with_capacity(dimensions.num_shards as usize);
+        for _ in 0..dimensions.num_shards {
+            handle_map_shards.push(HandleMapShard::default());
+        }
+        let handle_map_shards = handle_map_shards.into_boxed_slice();
+        Self {
+            dimensions,
+            handle_map_shards,
+            new_handle_id_counter: AtomicU64::new(0),
+            outstanding_allocations_counter: AtomicU32::new(0),
+        }
+    }
+}
+
+impl<T: Send + Sync> HandleMap<T> {
+    /// Allocates a new object within the given handle-map, returning
+    /// a handle to the location it was stored at. This operation
+    /// may fail if attempting to allocate over the `dimensions.max_active_handles`
+    /// limit imposed on the handle-map, in which case this method
+    /// will return a `HandleMapFullError`.
+    pub fn allocate(
+        &self,
+        initial_value_provider: impl FnOnce() -> T,
+    ) -> Result<Handle, HandleMapFullError> {
+        let mut initial_value_provider = initial_value_provider;
+        loop {
+            // Increment the new-handle-ID counter using relaxed memory ordering,
+            // since the only invariant that we want to enforce is that concurrently-running
+            // threads always get distinct new handle-ids.
+            let new_handle_id = self.new_handle_id_counter.fetch_add(1, Ordering::Relaxed);
+            let new_handle = Handle::from_id(new_handle_id);
+            let shard_index = new_handle.get_shard_index(self.dimensions.num_shards);
+
+            // Now, check the shard to see if we can actually allocate into it.
+            let shard_allocate_result = self.handle_map_shards[shard_index].try_allocate(
+                new_handle,
+                initial_value_provider,
+                &self.outstanding_allocations_counter,
+                self.dimensions.max_active_handles,
+            );
+            match shard_allocate_result {
+                Ok(_) => {
+                    return Ok(new_handle);
+                }
+                Err(ShardAllocationError::ExceedsAllocationLimit) => {
+                    return Err(HandleMapFullError);
+                }
+                Err(ShardAllocationError::EntryOccupied(thrown_back_provider)) => {
+                    // We need to do the whole thing again with a new ID
+                    initial_value_provider = thrown_back_provider;
+                }
+            }
+        }
+    }
+
+    /// Gets a read-only reference to an object within the given handle-map,
+    /// if the given handle is present. Otherwise, returns [`HandleNotPresentError`].
+    pub fn get(&self, handle: Handle) -> Result<ObjectReadGuardImpl<T>, HandleNotPresentError> {
+        let shard_index = handle.get_shard_index(self.dimensions.num_shards);
+        self.handle_map_shards[shard_index].get(handle)
+    }
+
+    /// Gets a read+write reference to an object within the given handle-map,
+    /// if the given handle is present. Otherwise, returns [`HandleNotPresentError`].
+    pub fn get_mut(
+        &self,
+        handle: Handle,
+    ) -> Result<ObjectReadWriteGuardImpl<T>, HandleNotPresentError> {
+        let shard_index = handle.get_shard_index(self.dimensions.num_shards);
+        self.handle_map_shards[shard_index].get_mut(handle)
+    }
+
+    /// Removes the object pointed to by the given handle in
+    /// the handle-map, returning the removed object if it
+    /// exists. Otherwise, returns [`HandleNotPresentError`].
+    pub fn deallocate(&self, handle: Handle) -> Result<T, HandleNotPresentError> {
+        let shard_index = handle.get_shard_index(self.dimensions.num_shards);
+        self.handle_map_shards[shard_index]
+            .deallocate(handle, &self.outstanding_allocations_counter)
+    }
+
+    /// Gets the actual number of elements stored in the entire map.
+    /// Only suitable for single-threaded sections of tests.
+    #[cfg(test)]
+    pub(crate) fn len(&self) -> usize {
+        self.handle_map_shards.iter().map(|s| s.len()).sum()
+    }
+
+    /// Sets the new-handle-id counter to the given value.
+    /// Only suitable for tests.
+    #[cfg(test)]
+    pub(crate) fn set_new_handle_id_counter(&mut self, value: u64) {
+        self.new_handle_id_counter = AtomicU64::new(value);
+    }
+}
+
+/// Externally-facing trait for things which behave like handle-map handles
+/// with a globally-defined handle-map for the type.
+pub trait HandleLike: Sized {
+    /// The underlying object type pointed-to by this handle
+    type Object: Send + Sync;
+
+    /// Tries to allocate a new handle using the given provider
+    /// to construct the underlying stored object as a new
+    /// entry into the global handle table for this type.
+    fn allocate(
+        initial_value_provider: impl FnOnce() -> Self::Object,
+    ) -> Result<Self, HandleMapFullError>;
+
+    /// Gets a RAII read-guard on the contents behind this handle.
+    fn get(&self) -> Result<ObjectReadGuardImpl<Self::Object>, HandleNotPresentError>;
+
+    /// Gets a RAII read-write guard on the contents behind this handle.
+    fn get_mut(&self) -> Result<ObjectReadWriteGuardImpl<Self::Object>, HandleNotPresentError>;
+
+    /// Deallocates the contents behind this handle.
+    fn deallocate(self) -> Result<Self::Object, HandleNotPresentError>;
+}
diff --git a/nearby/util/handle_map/src/shard.rs b/nearby/util/handle_map/src/shard.rs
new file mode 100644
index 0000000..da3bd57
--- /dev/null
+++ b/nearby/util/handle_map/src/shard.rs
@@ -0,0 +1,185 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use core::ops::{Deref, DerefMut};
+use lock_adapter::std::{RwLock, RwLockReadGuard, RwLockWriteGuard};
+use lock_adapter::RwLock as _;
+use std::collections::hash_map::Entry::{Occupied, Vacant};
+use std::collections::HashMap;
+use std::marker::PhantomData;
+use std::sync::atomic::{AtomicU32, Ordering};
+
+use crate::guard::{
+    ObjectReadGuardImpl, ObjectReadGuardMapping, ObjectReadWriteGuardImpl,
+    ObjectReadWriteGuardMapping,
+};
+use crate::{Handle, HandleNotPresentError};
+
+// Bunch o' type aliases to make talking about them much easier in the shard code.
+type ShardMapType<T> = HashMap<Handle, T>;
+type ShardReadWriteLock<T> = RwLock<ShardMapType<T>>;
+type ShardReadGuard<'a, T> = RwLockReadGuard<'a, ShardMapType<T>>;
+type ShardReadWriteGuard<'a, T> = RwLockWriteGuard<'a, ShardMapType<T>>;
+
+/// Internal error enum for failed allocations into a given shard.
+pub(crate) enum ShardAllocationError<T, F: FnOnce() -> T> {
+    /// Error for when the entry for the handle is occupied,
+    /// in which case we spit out the object-provider to try again
+    /// with a new handle-id.
+    EntryOccupied(F),
+    /// Error for when we would exceed the maximum number of allocations.
+    ExceedsAllocationLimit,
+}
+
+/// An individual handle-map shard, which is ultimately
+/// just a hash-map behind a lock.
+pub(crate) struct HandleMapShard<T: Send + Sync> {
+    data: RwLock<ShardMapType<T>>,
+}
+
+impl<T: Send + Sync> Default for HandleMapShard<T> {
+    fn default() -> Self {
+        Self { data: RwLock::new(HashMap::new()) }
+    }
+}
+
+impl<T: Send + Sync> HandleMapShard<T> {
+    pub fn get(&self, handle: Handle) -> Result<ObjectReadGuardImpl<T>, HandleNotPresentError> {
+        let map_read_guard = ShardReadWriteLock::<T>::read(&self.data);
+        let read_only_map_ref = map_read_guard.deref();
+        if read_only_map_ref.contains_key(&handle) {
+            let object_read_guard = ShardReadGuard::<T>::map(
+                map_read_guard,
+                ObjectReadGuardMapping { handle, _marker: PhantomData },
+            );
+            Ok(ObjectReadGuardImpl { guard: object_read_guard })
+        } else {
+            // Auto-drop the read guard, and return an error
+            Err(HandleNotPresentError)
+        }
+    }
+    /// Gets a read-write guard on the entire shard map if an entry for the given
+    /// handle exists, but if not, yield [`HandleNotPresentError`].
+    fn get_read_write_guard_if_entry_exists(
+        &self,
+        handle: Handle,
+    ) -> Result<ShardReadWriteGuard<T>, HandleNotPresentError> {
+        let contains_key = {
+            let map_ref = self.data.read();
+            map_ref.contains_key(&handle)
+        };
+        if contains_key {
+            // If we know that the entry exists, and we're currently
+            // holding a read-lock, we know that we're safe to request
+            // an upgrade to a write lock, since only one write or
+            // upgradable read lock can be outstanding at any one time.
+            let write_guard = self.data.write();
+            Ok(write_guard)
+        } else {
+            // Auto-drop the read guard, we don't need to allow a write.
+            Err(HandleNotPresentError)
+        }
+    }
+
+    pub fn get_mut(
+        &self,
+        handle: Handle,
+    ) -> Result<ObjectReadWriteGuardImpl<T>, HandleNotPresentError> {
+        let map_read_write_guard = self.get_read_write_guard_if_entry_exists(handle)?;
+        // Expose only the pointed-to object with a mapped read-write guard
+        let object_read_write_guard = ShardReadWriteGuard::<T>::map(
+            map_read_write_guard,
+            ObjectReadWriteGuardMapping { handle, _marker: PhantomData },
+        );
+        Ok(ObjectReadWriteGuardImpl { guard: object_read_write_guard })
+    }
+
+    pub fn deallocate(
+        &self,
+        handle: Handle,
+        outstanding_allocations_counter: &AtomicU32,
+    ) -> Result<T, HandleNotPresentError> {
+        let mut map_read_write_guard = self.get_read_write_guard_if_entry_exists(handle)?;
+        // We don't need to worry about double-decrements, since the above call
+        // got us an upgradable read guard for our read, which means it's the only
+        // outstanding upgradeable guard on the shard. See `spin` documentation.
+        // Remove the pointed-to object from the map, and return it,
+        // releasing the lock when the guard goes out of scope.
+        let removed_object = map_read_write_guard.deref_mut().remove(&handle).unwrap();
+        // Decrement the allocations counter. Release ordering because we want
+        // to ensure that clearing the map entry never gets re-ordered to after when
+        // this counter gets decremented.
+        let _ = outstanding_allocations_counter.fetch_sub(1, Ordering::Release);
+        Ok(removed_object)
+    }
+
+    pub fn try_allocate<F>(
+        &self,
+        handle: Handle,
+        object_provider: F,
+        outstanding_allocations_counter: &AtomicU32,
+        max_active_handles: u32,
+    ) -> Result<(), ShardAllocationError<T, F>>
+    where
+        F: FnOnce() -> T,
+    {
+        let mut read_write_guard = self.data.write();
+        match read_write_guard.entry(handle) {
+            Occupied(_) => {
+                // We've already allocated for that handle-id, so yield
+                // the object provider back to the caller.
+                Err(ShardAllocationError::EntryOccupied(object_provider))
+            }
+            Vacant(vacant_entry) => {
+                // An entry is open, but we haven't yet checked the allocations count.
+                // Try to increment the total allocations count atomically.
+                // Use acquire ordering on a successful bump, because we don't want
+                // to invoke the allocation closure before we have a guaranteed slot.
+                // On the other hand, upon failure, we don't care about ordering
+                // of surrounding operations, and so we use a relaxed ordering there.
+                let allocation_count_bump_result = outstanding_allocations_counter.fetch_update(
+                    Ordering::Acquire,
+                    Ordering::Relaxed,
+                    |old_total_allocations| {
+                        if old_total_allocations >= max_active_handles {
+                            None
+                        } else {
+                            Some(old_total_allocations + 1)
+                        }
+                    },
+                );
+                match allocation_count_bump_result {
+                    Ok(_) => {
+                        // We're good to actually allocate
+                        let object = object_provider();
+                        vacant_entry.insert(object);
+                        Ok(())
+                    }
+                    Err(_) => {
+                        // The allocation would cause us to exceed the allowed allocations,
+                        // so release all locks and error.
+                        Err(ShardAllocationError::ExceedsAllocationLimit)
+                    }
+                }
+            }
+        }
+    }
+    /// Gets the actual number of elements stored in this shard.
+    /// Only suitable for single-threaded sections of tests.
+    #[cfg(test)]
+    pub fn len(&self) -> usize {
+        let guard = ShardReadWriteLock::<T>::read(&self.data);
+        guard.deref().len()
+    }
+}
diff --git a/nearby/presence/handle_map/src/tests.rs b/nearby/util/handle_map/src/tests.rs
similarity index 99%
rename from nearby/presence/handle_map/src/tests.rs
rename to nearby/util/handle_map/src/tests.rs
index d97657f..a0774d6 100644
--- a/nearby/presence/handle_map/src/tests.rs
+++ b/nearby/util/handle_map/src/tests.rs
@@ -13,7 +13,8 @@
 // limitations under the License.
 use crate::*;
 
-use hashbrown::HashSet;
+use core::ops::{Deref, DerefMut};
+use std::collections::HashSet;
 use std::sync::Arc;
 use std::thread;
 
diff --git a/nearby/connections/ukey2/lock_adapter/Cargo.toml b/nearby/util/lock_adapter/Cargo.toml
similarity index 91%
rename from nearby/connections/ukey2/lock_adapter/Cargo.toml
rename to nearby/util/lock_adapter/Cargo.toml
index 713f224..eae1fc9 100644
--- a/nearby/connections/ukey2/lock_adapter/Cargo.toml
+++ b/nearby/util/lock_adapter/Cargo.toml
@@ -8,6 +8,6 @@
 spin = { workspace = true, optional = true }
 
 [features]
-default = ["spin"]
-spin = ["dep:spin"]
+default = ["std"]
 std = []
+spin = ["dep:spin"]
diff --git a/nearby/connections/ukey2/lock_adapter/src/lib.rs b/nearby/util/lock_adapter/src/lib.rs
similarity index 72%
rename from nearby/connections/ukey2/lock_adapter/src/lib.rs
rename to nearby/util/lock_adapter/src/lib.rs
index 098031b..3d75c0d 100644
--- a/nearby/connections/ukey2/lock_adapter/src/lib.rs
+++ b/nearby/util/lock_adapter/src/lib.rs
@@ -12,6 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+//! An abstraction layer for Rust synchronization primitives which provides both no_std and std library
+//! based implementations
+
+#![forbid(unsafe_code)]
+#![deny(missing_docs)]
 #![cfg_attr(not(feature = "std"), no_std)]
 
 /// A Spinlock-based implementation of Mutex using the `spin` crate that can be used in `no_std`
@@ -58,3 +63,23 @@
     /// Creates a new mutex in an unlocked state ready for use.
     fn new(value: T) -> Self;
 }
+
+/// A reader-writer lock. This type of lock allows a number of readers or at most one writer at
+/// any point in time.
+pub trait RwLock<T> {
+    /// RAII structure used to release the shared read access of a lock when dropped.
+    type RwLockReadGuard<'a>
+    where
+        Self: 'a;
+
+    /// RAII structure used to release the exclusive write access of a lock when dropped.
+    type RwLockWriteGuard<'a>
+    where
+        Self: 'a;
+
+    /// Locks this RwLock with shared read access, blocking the current thread until it can be acquired.
+    fn read(&self) -> Self::RwLockReadGuard<'_>;
+
+    /// Locks this RwLock with exclusive write access, blocking the current thread until it can be acquired.
+    fn write(&self) -> Self::RwLockWriteGuard<'_>;
+}
diff --git a/nearby/util/lock_adapter/src/spin.rs b/nearby/util/lock_adapter/src/spin.rs
new file mode 100644
index 0000000..e894cef
--- /dev/null
+++ b/nearby/util/lock_adapter/src/spin.rs
@@ -0,0 +1,60 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::NoPoisonMutex;
+
+/// A mutual exclusion primitive useful for protecting shared data
+pub struct Mutex<T>(spin::Mutex<T>);
+
+impl<T> NoPoisonMutex<T> for Mutex<T> {
+    type MutexGuard<'a> = spin::MutexGuard<'a, T> where T: 'a;
+
+    fn lock(&self) -> Self::MutexGuard<'_> {
+        self.0.lock()
+    }
+
+    fn try_lock(&self) -> Option<Self::MutexGuard<'_>> {
+        self.0.try_lock()
+    }
+
+    fn new(value: T) -> Self {
+        Self(spin::Mutex::new(value))
+    }
+}
+
+/// A reader-writer lock
+/// This type of lock allows a number of readers or at most one writer at any point in time.
+/// The write portion of this lock typically allows modification of the underlying data (exclusive access)
+/// and the read portion of this lock typically allows for read-only access (shared access).
+pub struct RwLock<T>(spin::RwLock<T>);
+
+impl<T> RwLock<T> {
+    /// Creates a new instance of an `RwLock<T>` which is unlocked.
+    pub const fn new(inner: T) -> Self {
+        Self(spin::RwLock::new(inner))
+    }
+}
+
+impl<T> crate::RwLock<T> for RwLock<T> {
+    type RwLockReadGuard<'a> = spin::RwLockReadGuard<'a, T> where T: 'a;
+    type RwLockWriteGuard<'a> = spin::RwLockWriteGuard<'a, T> where T: 'a;
+
+    fn read(&self) -> Self::RwLockReadGuard<'_> {
+        self.0.read()
+    }
+
+    fn write(&self) -> Self::RwLockWriteGuard<'_> {
+        self.0.write()
+    }
+}
diff --git a/nearby/util/lock_adapter/src/std.rs b/nearby/util/lock_adapter/src/std.rs
new file mode 100644
index 0000000..c950d85
--- /dev/null
+++ b/nearby/util/lock_adapter/src/std.rs
@@ -0,0 +1,175 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::NoPoisonMutex;
+use std::ops::{Deref, DerefMut};
+
+/// A mutual exclusion primitive useful for protecting shared data
+pub struct Mutex<T>(std::sync::Mutex<T>);
+
+impl<T> NoPoisonMutex<T> for Mutex<T> {
+    type MutexGuard<'a> = std::sync::MutexGuard<'a, T> where T: 'a;
+
+    fn lock(&self) -> Self::MutexGuard<'_> {
+        self.0.lock().unwrap_or_else(|poison| poison.into_inner())
+    }
+
+    fn try_lock(&self) -> Option<Self::MutexGuard<'_>> {
+        match self.0.try_lock() {
+            Ok(guard) => Some(guard),
+            Err(std::sync::TryLockError::Poisoned(guard)) => Some(guard.into_inner()),
+            Err(std::sync::TryLockError::WouldBlock) => None,
+        }
+    }
+
+    fn new(value: T) -> Self {
+        Self(std::sync::Mutex::new(value))
+    }
+}
+
+/// A reader-writer lock
+/// This type of lock allows a number of readers or at most one writer at any point in time.
+/// The write portion of this lock typically allows modification of the underlying data (exclusive access)
+/// and the read portion of this lock typically allows for read-only access (shared access).
+pub struct RwLock<T>(std::sync::RwLock<T>);
+
+impl<T> RwLock<T> {
+    /// Creates a new instance of an `RwLock<T>` which is unlocked.
+    pub const fn new(value: T) -> Self {
+        Self(std::sync::RwLock::new(value))
+    }
+}
+
+impl<T> crate::RwLock<T> for RwLock<T> {
+    type RwLockReadGuard<'a> = RwLockReadGuard<'a, T> where T: 'a;
+
+    type RwLockWriteGuard<'a> = RwLockWriteGuard<'a, T> where T: 'a;
+
+    fn read(&self) -> Self::RwLockReadGuard<'_> {
+        RwLockReadGuard(self.0.read().unwrap_or_else(|e| e.into_inner()))
+    }
+
+    fn write(&self) -> Self::RwLockWriteGuard<'_> {
+        RwLockWriteGuard(self.0.write().unwrap_or_else(|e| e.into_inner()))
+    }
+}
+
+/// RAII structure used to release the shared read access of a lock when dropped.
+pub struct RwLockReadGuard<'a, T>(std::sync::RwLockReadGuard<'a, T>);
+
+impl<'a, T> RwLockReadGuard<'a, T> {
+    /// Make a new MappedRwLockReadGuard for a component of the locked data.
+    pub fn map<U, M>(s: Self, mapping: M) -> MappedRwLockReadGuard<'a, T, U, M>
+    where
+        M: RwMapping<Arg = T, Ret = U>,
+    {
+        MappedRwLockReadGuard { mapping, guard: s }
+    }
+}
+
+impl<'a, T> Deref for RwLockReadGuard<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.0.deref()
+    }
+}
+
+/// An RAII read lock guard returned by RwLockReadGuard::map, which can point to a subfield of the protected data.
+pub struct MappedRwLockReadGuard<'a, T, U, M>
+where
+    M: RwMapping<Arg = T, Ret = U>,
+{
+    mapping: M,
+    guard: RwLockReadGuard<'a, T>,
+}
+
+impl<'a, T, U, M> Deref for MappedRwLockReadGuard<'a, T, U, M>
+where
+    M: RwMapping<Arg = T, Ret = U>,
+{
+    type Target = U;
+
+    fn deref(&self) -> &Self::Target {
+        self.mapping.map(&*self.guard)
+    }
+}
+
+/// RAII structure used to release the exclusive write access of a lock when dropped.
+pub struct RwLockWriteGuard<'a, T>(std::sync::RwLockWriteGuard<'a, T>);
+
+impl<'a, T> RwLockWriteGuard<'a, T> {
+    /// Make a new MappedRwLockWriteGuard for a component of the locked data.
+    pub fn map<U, M>(s: Self, mapping: M) -> MappedRwLockWriteGuard<'a, T, U, M>
+    where
+        M: RwMapping<Arg = T, Ret = U>,
+    {
+        MappedRwLockWriteGuard { mapping, guard: s }
+    }
+}
+
+impl<'a, T> Deref for RwLockWriteGuard<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.0.deref()
+    }
+}
+
+impl<'a, T> DerefMut for RwLockWriteGuard<'a, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.0.deref_mut()
+    }
+}
+
+/// An RAII read lock guard returned by RwLockWriteGuard::map, which can point to a subfield of the protected data.
+pub struct MappedRwLockWriteGuard<'a, T, U, M>
+where
+    M: RwMapping<Arg = T, Ret = U>,
+{
+    mapping: M,
+    guard: RwLockWriteGuard<'a, T>,
+}
+
+impl<'a, P, T, M> Deref for MappedRwLockWriteGuard<'a, P, T, M>
+where
+    M: RwMapping<Arg = P, Ret = T>,
+{
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.mapping.map(&*self.guard)
+    }
+}
+
+impl<'a, P, T, M> DerefMut for MappedRwLockWriteGuard<'a, P, T, M>
+where
+    M: RwMapping<Arg = P, Ret = T>,
+{
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.mapping.map_mut(&mut *self.guard)
+    }
+}
+
+/// Mapping functions which define how to map from one locked data type to a component of that locked data
+pub trait RwMapping {
+    /// The original locked data type
+    type Arg;
+    /// The returned mapped locked data type which is a component of the original locked data
+    type Ret;
+    /// Maps from Arg into Ret
+    fn map<'a>(&self, arg: &'a Self::Arg) -> &'a Self::Ret;
+    /// Mutably maps from Arg into Ret
+    fn map_mut<'a>(&self, arg: &'a mut Self::Arg) -> &'a mut Self::Ret;
+}
diff --git a/remoteauth/Cargo.lock b/remoteauth/Cargo.lock
new file mode 100644
index 0000000..55841b9
--- /dev/null
+++ b/remoteauth/Cargo.lock
@@ -0,0 +1,469 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
+
+[[package]]
+name = "bitflags"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+
+[[package]]
+name = "build-scripts"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "cmd-runner",
+ "env_logger",
+ "log",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "clap"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
+
+[[package]]
+name = "cmd-runner"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "owo-colors",
+ "shell-escape",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "ctap_protocol"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+dependencies = [
+ "hermit-abi",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "memchr"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f478948fd84d9f8e86967bf432640e46adfb5a4bd4f14ef7e864ab38220534ae"
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "owo-colors"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+[[package]]
+name = "platform"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
+
+[[package]]
+name = "remote_auth_protocol"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "2.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
diff --git a/remoteauth/Cargo.toml b/remoteauth/Cargo.toml
new file mode 100644
index 0000000..20b2ec3
--- /dev/null
+++ b/remoteauth/Cargo.toml
@@ -0,0 +1,27 @@
+[workspace]
+members = [
+    "ctap_protocol",
+    "platform",
+    "remote_auth_protocol",
+]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[workspace.dependencies]
+anyhow = "1.0.72"
+
+[package]
+name = "build-scripts"
+version.workspace = true
+edition.workspace = true
+publish.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+clap = { version = "4.0.25", features = ["derive"] }
+cmd-runner = { path = "../cmd-runner"  }
+env_logger = "0.10.0"
+log = "0.4.17"
\ No newline at end of file
diff --git a/nearby/crypto/bssl-crypto/Cargo.toml b/remoteauth/ctap_protocol/Cargo.toml
similarity index 80%
copy from nearby/crypto/bssl-crypto/Cargo.toml
copy to remoteauth/ctap_protocol/Cargo.toml
index bfe3964..8a06840 100644
--- a/nearby/crypto/bssl-crypto/Cargo.toml
+++ b/remoteauth/ctap_protocol/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "bssl-crypto"
+name = "ctap_protocol"
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
@@ -7,3 +7,4 @@
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+anyhow.workspace = true
diff --git a/remoteauth/ctap_protocol/src/command.rs b/remoteauth/ctap_protocol/src/command.rs
new file mode 100644
index 0000000..e512755
--- /dev/null
+++ b/remoteauth/ctap_protocol/src/command.rs
@@ -0,0 +1,227 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! # CTAP Protocol
+//!
+//! This crate represents CTAP messages and turns them into a binary representation to be sent to a
+//! remote device.
+
+use anyhow::anyhow;
+
+/// The Rust representation of CTAP values (or identifiers).
+#[derive(Debug, PartialEq)]
+pub enum Value {
+    AuthenticatorMakeCredential = 0x01,
+    AuthenticatorGetAssertion = 0x02,
+    AuthenticatorGetInfo = 0x04,
+    AuthenticatorClientPIN = 0x06,
+    AuthenticatorReset = 0x07,
+    AuthenticatorGetNextAssertion = 0x08,
+}
+
+/// The Rust representation of CTAP parameters.
+#[derive(Debug, PartialEq)]
+pub enum Parameters {
+    AuthenticatorMakeCredential,
+    AuthenticatorGetAssertion,
+    AuthenticatorClientPIN,
+}
+
+impl From<Parameters> for Vec<u8> {
+    fn from(message: Parameters) -> Vec<u8> {
+        // TODO: serialize parameters correctly
+        match message {
+            Parameters::AuthenticatorMakeCredential => vec![],
+            Parameters::AuthenticatorGetAssertion => vec![],
+            Parameters::AuthenticatorClientPIN => vec![],
+        }
+    }
+}
+
+pub struct Command {
+    pub value: Value,
+    pub parameters: Option<Parameters>,
+}
+
+impl From<Command> for Vec<u8> {
+    /// Converts the given CTAP command into its binary representation.
+    fn from(message: Command) -> Vec<u8> {
+        let mut result = vec![message.value as u8];
+        if let Some(p) = message.parameters {
+            result.append(&mut p.into());
+        }
+        result
+    }
+}
+
+impl TryFrom<Vec<u8>> for Command {
+    type Error = anyhow::Error;
+    /// Convert a binary message to its Rust representation.
+    fn try_from(bytes: Vec<u8>) -> anyhow::Result<Self> {
+        if bytes.is_empty() {
+            Err(anyhow!("Binary message was empty."))
+        } else {
+            match bytes[0] {
+                _x if _x == Value::AuthenticatorMakeCredential as u8 => Ok(Self {
+                    value: Value::AuthenticatorMakeCredential,
+                    parameters: Some(Parameters::AuthenticatorMakeCredential),
+                }),
+                _x if _x == Value::AuthenticatorGetAssertion as u8 => Ok(Self {
+                    value: Value::AuthenticatorGetAssertion,
+                    parameters: Some(Parameters::AuthenticatorGetAssertion),
+                }),
+                _x if _x == Value::AuthenticatorGetInfo as u8 => Ok(Self {
+                    value: Value::AuthenticatorGetInfo,
+                    parameters: None,
+                }),
+                _x if _x == Value::AuthenticatorClientPIN as u8 => Ok(Self {
+                    value: Value::AuthenticatorClientPIN,
+                    parameters: Some(Parameters::AuthenticatorClientPIN),
+                }),
+                _x if _x == Value::AuthenticatorReset as u8 => Ok(Self {
+                    value: Value::AuthenticatorReset,
+                    parameters: None,
+                }),
+                _x if _x == Value::AuthenticatorGetNextAssertion as u8 => Ok(Self {
+                    value: Value::AuthenticatorGetNextAssertion,
+                    parameters: None,
+                }),
+                _ => Err(anyhow!("Unknown message type.")),
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use anyhow::bail;
+
+    #[test]
+    fn translate_message_to_bytes() {
+        let mut message = Command {
+            value: Value::AuthenticatorReset,
+            parameters: None,
+        };
+        let mut bytes: Vec<u8> = message.into();
+
+        assert_eq!(bytes, vec![Value::AuthenticatorReset as u8]);
+
+        message = Command {
+            value: Value::AuthenticatorMakeCredential,
+            parameters: None,
+        };
+        bytes = message.into();
+
+        assert_eq!(bytes, vec![Value::AuthenticatorMakeCredential as u8]);
+
+        message = Command {
+            value: Value::AuthenticatorGetAssertion,
+            parameters: None,
+        };
+        bytes = message.into();
+
+        assert_eq!(bytes, vec![Value::AuthenticatorGetAssertion as u8]);
+
+        message = Command {
+            value: Value::AuthenticatorGetInfo,
+            parameters: None,
+        };
+        bytes = message.into();
+
+        assert_eq!(bytes, vec![Value::AuthenticatorGetInfo as u8]);
+
+        message = Command {
+            value: Value::AuthenticatorClientPIN,
+            parameters: None,
+        };
+        bytes = message.into();
+
+        assert_eq!(bytes, vec![Value::AuthenticatorClientPIN as u8]);
+
+        message = Command {
+            value: Value::AuthenticatorGetNextAssertion,
+            parameters: None,
+        };
+        bytes = message.into();
+
+        assert_eq!(bytes, vec![Value::AuthenticatorGetNextAssertion as u8]);
+    }
+
+    #[test]
+    fn translate_bytes_to_message() -> anyhow::Result<()> {
+        let mut bytes = vec![Value::AuthenticatorReset as u8];
+        let mut message: Command = bytes.try_into()?;
+        assert_eq!(message.value, Value::AuthenticatorReset);
+        assert_eq!(message.parameters, None);
+
+        bytes = vec![Value::AuthenticatorMakeCredential as u8];
+        message = bytes.try_into()?;
+        assert_eq!(message.value, Value::AuthenticatorMakeCredential);
+        assert_eq!(
+            message.parameters,
+            Some(Parameters::AuthenticatorMakeCredential)
+        );
+
+        bytes = vec![Value::AuthenticatorGetAssertion as u8];
+        message = bytes.try_into()?;
+        assert_eq!(message.value, Value::AuthenticatorGetAssertion);
+        assert_eq!(
+            message.parameters,
+            Some(Parameters::AuthenticatorGetAssertion)
+        );
+
+        bytes = vec![Value::AuthenticatorGetInfo as u8];
+        message = bytes.try_into()?;
+        assert_eq!(message.value, Value::AuthenticatorGetInfo);
+        assert_eq!(message.parameters, None);
+
+        bytes = vec![Value::AuthenticatorClientPIN as u8];
+        message = bytes.try_into()?;
+        assert_eq!(message.value, Value::AuthenticatorClientPIN);
+        assert_eq!(message.parameters, Some(Parameters::AuthenticatorClientPIN));
+
+        bytes = vec![Value::AuthenticatorGetNextAssertion as u8];
+        message = bytes.try_into()?;
+        assert_eq!(message.value, Value::AuthenticatorGetNextAssertion);
+        assert_eq!(message.parameters, None);
+        Ok(())
+    }
+
+    #[test]
+    fn translate_empty_bytes() -> anyhow::Result<()> {
+        let bytes = vec![];
+        let res: anyhow::Result<Command> = bytes.try_into();
+        match res {
+            Err(e) => {
+                assert_eq!(e.to_string(), "Binary message was empty.");
+                Ok(())
+            }
+            _ => bail!("Should not parse empty bytes"),
+        }
+    }
+
+    #[test]
+    fn translate_unknown_message() -> anyhow::Result<()> {
+        let bytes = vec![0x10];
+        let res: anyhow::Result<Command> = bytes.try_into();
+        match res {
+            Err(e) => {
+                assert_eq!(e.to_string(), "Unknown message type.");
+                Ok(())
+            }
+            _ => bail!("Should not parse unknown message."),
+        }
+    }
+}
diff --git a/nearby/crypto/bssl-crypto/src/lib.rs b/remoteauth/ctap_protocol/src/lib.rs
similarity index 78%
rename from nearby/crypto/bssl-crypto/src/lib.rs
rename to remoteauth/ctap_protocol/src/lib.rs
index 89e6968..b42193e 100644
--- a/nearby/crypto/bssl-crypto/src/lib.rs
+++ b/remoteauth/ctap_protocol/src/lib.rs
@@ -12,5 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Placeholder crate to satisfy cargo. If actually using boring ssl, please run the
-//! `build-boringssl` subcommand of the top level crate.
+//! # CTAP Protocol
+//!
+//! This crate represents CTAP messages and turns them into a binary representation to be sent to a
+//! remote device.
+
+pub mod command;
diff --git a/remoteauth/deny.toml b/remoteauth/deny.toml
new file mode 100644
index 0000000..2bf5920
--- /dev/null
+++ b/remoteauth/deny.toml
@@ -0,0 +1,219 @@
+# This template contains all of the possible sections and their default values
+
+# Note that all fields that take a lint level have these possible values:
+# * deny - An error will be produced and the check will fail
+# * warn - A warning will be produced, but the check will not fail
+# * allow - No warning or error will be produced, though in some cases a note
+# will be
+
+# The values provided in this template are the default values that will be used
+# when any section or field is not specified in your own configuration
+
+# If 1 or more target triples (and optionally, target_features) are specified,
+# only the specified targets will be checked when running `cargo deny check`.
+# This means, if a particular package is only ever used as a target specific
+# dependency, such as, for example, the `nix` crate only being used via the
+# `target_family = "unix"` configuration, that only having windows targets in
+# this list would mean the nix crate, as well as any of its exclusive
+# dependencies not shared by any other crates, would be ignored, as the target
+# list here is effectively saying which targets you are building for.
+targets = [
+    # The triple can be any string, but only the target triples built in to
+    # rustc (as of 1.40) can be checked against actual config expressions
+    #{ triple = "x86_64-unknown-linux-musl" },
+    # You can also specify which target_features you promise are enabled for a
+    # particular target. target_features are currently not validated against
+    # the actual valid features supported by the target architecture.
+    #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
+]
+
+# This section is considered when running `cargo deny check advisories`
+# More documentation for the advisories section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
+[advisories]
+# The path where the advisory database is cloned/fetched into
+db-path = "~/.cargo/advisory-db"
+# The url(s) of the advisory databases to use
+db-urls = ["https://github.com/rustsec/advisory-db"]
+# The lint level for security vulnerabilities
+vulnerability = "deny"
+# The lint level for unmaintained crates
+unmaintained = "warn"
+# The lint level for crates that have been yanked from their source registry
+yanked = "warn"
+# The lint level for crates with security notices. Note that as of
+# 2019-12-17 there are no security notice advisories in
+# https://github.com/rustsec/advisory-db
+notice = "warn"
+# A list of advisory IDs to ignore. Note that ignored advisories will still
+# output a note when they are encountered.
+ignore = [
+    # criterion 0.4.0 depends on a version of atty w/unaligned reads
+]
+# Threshold for security vulnerabilities, any vulnerability with a CVSS score
+# lower than the range specified will be ignored. Note that ignored advisories
+# will still output a note when they are encountered.
+# * None - CVSS Score 0.0
+# * Low - CVSS Score 0.1 - 3.9
+# * Medium - CVSS Score 4.0 - 6.9
+# * High - CVSS Score 7.0 - 8.9
+# * Critical - CVSS Score 9.0 - 10.0
+#severity-threshold =
+
+# If this is true, then cargo deny will use the git executable to fetch advisory database.
+# If this is false, then it uses a built-in git library.
+# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
+# See Git Authentication for more information about setting up git authentication.
+#git-fetch-with-cli = true
+
+# This section is considered when running `cargo deny check licenses`
+# More documentation for the licenses section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
+[licenses]
+# The lint level for crates which do not have a detectable license
+unlicensed = "deny"
+unused-allowed-license = "allow"
+# List of explicitly allowed licenses
+# See https://spdx.org/licenses/ for list of possible licenses
+# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
+allow = [
+    "MIT",
+    "Apache-2.0",
+    "Apache-2.0 WITH LLVM-exception",
+    "BSD-3-Clause",
+    "BSD-2-Clause",
+    "ISC",
+    "Unicode-DFS-2016",
+    "OpenSSL",
+    "Unlicense"
+]
+# List of explicitly disallowed licenses
+# See https://spdx.org/licenses/ for list of possible licenses
+# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
+deny = [
+    #"Nokia",
+]
+# Lint level for licenses considered copyleft
+copyleft = "warn"
+# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
+# * both - The license will be approved if it is both OSI-approved *AND* FSF
+# * either - The license will be approved if it is either OSI-approved *OR* FSF
+# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
+# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
+# * neither - This predicate is ignored and the default lint level is used
+allow-osi-fsf-free = "neither"
+# Lint level used when no other predicates are matched
+# 1. License isn't in the allow or deny lists
+# 2. License isn't copyleft
+# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
+default = "deny"
+# The confidence threshold for detecting a license from license text.
+# The higher the value, the more closely the license text must be to the
+# canonical license text of a valid SPDX license file.
+# [possible values: any between 0.0 and 1.0].
+confidence-threshold = 0.8
+# Allow 1 or more licenses on a per-crate basis, so that particular licenses
+# aren't accepted for every possible crate as with the normal allow list
+exceptions = [
+    # Each entry is the crate and version constraint, and its specific allow
+    # list
+    #{ allow = ["Zlib"], name = "adler32", version = "*" },
+]
+
+# Some crates don't have (easily) machine readable licensing information,
+# adding a clarification entry for it allows you to manually specify the
+# licensing information
+#[[licenses.clarify]]
+# The name of the crate the clarification applies to
+#name = "ring"
+# The optional version constraint for the crate
+#version = "*"
+# The SPDX expression for the license requirements of the crate
+#expression = "MIT AND ISC AND OpenSSL"
+# One or more files in the crate's source used as the "source of truth" for
+# the license expression. If the contents match, the clarification will be used
+# when running the license check, otherwise the clarification will be ignored
+# and the crate will be checked normally, which may produce warnings or errors
+# depending on the rest of your configuration
+#license-files = [
+    # Each entry is a crate relative path, and the (opaque) hash of its contents
+    #{ path = "LICENSE", hash = 0xbd0eed23 }
+#]
+
+[[licenses.clarify]]
+name = "ring"
+version = "*"
+expression = "MIT AND ISC AND OpenSSL"
+license-files = [
+    # Each entry is a crate relative path, and the (opaque) hash of its contents
+    { path = "LICENSE", hash = 0xbd0eed23 }
+]
+
+[licenses.private]
+# If true, ignores workspace crates that aren't published, or are only
+# published to private registries.
+# To see how to mark a crate as unpublished (to the official registry),
+# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
+ignore = true
+# One or more private registries that you might publish crates to, if a crate
+# is only published to private registries, and ignore is true, the crate will
+# not have its license(s) checked
+registries = [
+    #"https://sekretz.com/registry
+]
+
+# This section is considered when running `cargo deny check bans`.
+# More documentation about the 'bans' section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
+[bans]
+# Lint level for when multiple versions of the same crate are detected
+multiple-versions = "allow"
+# Lint level for when a crate version requirement is `*`
+wildcards = "allow"
+# The graph highlighting used when creating dotgraphs for crates
+# with multiple versions
+# * lowest-version - The path to the lowest versioned duplicate is highlighted
+# * simplest-path - The path to the version with the fewest edges is highlighted
+# * all - Both lowest-version and simplest-path are used
+highlight = "all"
+# List of crates that are allowed. Use with care!
+allow = [
+    #{ name = "ansi_term", version = "=0.11.0" },
+]
+# List of crates to deny
+deny = [
+    # Each entry the name of a crate and a version range. If version is
+    # not specified, all versions will be matched.
+    #{ name = "ansi_term", version = "=0.11.0" },
+    #
+    # Wrapper crates can optionally be specified to allow the crate when it
+    # is a direct dependency of the otherwise banned crate
+    #{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
+]
+# Certain crates/versions that will be skipped when doing duplicate detection.
+skip = [
+    #{ name = "ansi_term", version = "=0.11.0" },
+]
+# Similarly to `skip` allows you to skip certain crates during duplicate
+# detection. Unlike skip, it also includes the entire tree of transitive
+# dependencies starting at the specified crate, up to a certain depth, which is
+# by default infinite
+skip-tree = [
+    #{ name = "ansi_term", version = "=0.11.0", depth = 20 },
+]
+
+# This section is considered when running `cargo deny check sources`.
+# More documentation about the 'sources' section can be found here:
+# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
+[sources]
+# Lint level for what to happen when a crate from a crate registry that is not
+# in the allow list is encountered
+unknown-registry = "warn"
+# Lint level for what to happen when a crate from a git repository that is not
+# in the allow list is encountered
+unknown-git = "warn"
+# List of URLs for allowed crate registries. Defaults to the crates.io index
+# if not specified. If it is specified but empty, no registries are allowed.
+allow-registry = ["https://github.com/rust-lang/crates.io-index"]
+# List of URLs for allowed Git repositories
+allow-git = []
\ No newline at end of file
diff --git a/nearby/crypto/bssl-crypto/Cargo.toml b/remoteauth/platform/Cargo.toml
similarity index 82%
copy from nearby/crypto/bssl-crypto/Cargo.toml
copy to remoteauth/platform/Cargo.toml
index bfe3964..0136c27 100644
--- a/nearby/crypto/bssl-crypto/Cargo.toml
+++ b/remoteauth/platform/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "bssl-crypto"
+name = "platform"
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
@@ -7,3 +7,4 @@
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+anyhow.workspace = true
diff --git a/remoteauth/platform/src/lib.rs b/remoteauth/platform/src/lib.rs
new file mode 100644
index 0000000..798c756
--- /dev/null
+++ b/remoteauth/platform/src/lib.rs
@@ -0,0 +1,29 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+pub mod listeners;
+
+use crate::listeners::SendRequestListener;
+
+/// # RemoteAuth Platform
+/// This trait represents the capabilities that RemoteAuth requires from the platform.
+pub trait Platform {
+    /// Send a binary message to the remote with the given connection id and return the response.
+    fn send_request(
+        &self,
+        connection_id: i32,
+        request: &[u8],
+        listener: Box<dyn SendRequestListener + Send>,
+    );
+}
diff --git a/nearby/crypto/bssl-crypto/src/lib.rs b/remoteauth/platform/src/listeners.rs
similarity index 71%
copy from nearby/crypto/bssl-crypto/src/lib.rs
copy to remoteauth/platform/src/listeners.rs
index 89e6968..e354fea 100644
--- a/nearby/crypto/bssl-crypto/src/lib.rs
+++ b/remoteauth/platform/src/listeners.rs
@@ -12,5 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//! Placeholder crate to satisfy cargo. If actually using boring ssl, please run the
-//! `build-boringssl` subcommand of the top level crate.
+/// SendRequestListener handles the result or an error from calling send_request in the Platform.
+pub trait SendRequestListener {
+    fn on_response(&mut self, response: Vec<u8>);
+    fn on_error(&mut self, error_code: i32);
+}
diff --git a/nearby/crypto/bssl-crypto/Cargo.toml b/remoteauth/remote_auth_protocol/Cargo.toml
similarity index 78%
copy from nearby/crypto/bssl-crypto/Cargo.toml
copy to remoteauth/remote_auth_protocol/Cargo.toml
index bfe3964..e9fd6d2 100644
--- a/nearby/crypto/bssl-crypto/Cargo.toml
+++ b/remoteauth/remote_auth_protocol/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "bssl-crypto"
+name = "remote_auth_protocol"
 version.workspace = true
 edition.workspace = true
 publish.workspace = true
@@ -7,3 +7,4 @@
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+anyhow.workspace = true
diff --git a/remoteauth/remote_auth_protocol/src/lib.rs b/remoteauth/remote_auth_protocol/src/lib.rs
new file mode 100644
index 0000000..bc8315b
--- /dev/null
+++ b/remoteauth/remote_auth_protocol/src/lib.rs
@@ -0,0 +1,42 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+pub mod remote_auth_service;
+
+/// Struct representing the remote device.
+pub struct RemoteDevice {
+    pub id: i32,
+}
+
+/// Trait to be implemented by anything that wants to be notified when remote devices are discovered
+/// or lost.
+pub trait DeviceDiscoveryListener {
+    fn on_discovered(&mut self, remote_device: &RemoteDevice);
+    fn on_lost(&mut self, remote_device: &RemoteDevice);
+    fn on_timeout(&mut self);
+}
+
+/// Trait to be implemented by the struct responsible for scanning for remote devices.
+pub trait DiscoveryPublisher<'a, T: DeviceDiscoveryListener> {
+    /// Add a listener to the list and return a handle to that listener so it can be removed later.
+    fn add_listener(&mut self, listener: &'a mut T) -> i32;
+    /// Remove the listener with the given handle.
+    fn remove_listener(&mut self, listener_handle: i32);
+    /// Notify all listeners that a remote device has been discovered.
+    fn device_discovered(&mut self, remote_device: &RemoteDevice);
+    /// Notify all listeners that a previously discovered remote device has been lost.
+    fn device_lost(&mut self, remote_device: &RemoteDevice);
+    /// Notify all listeners that timeout has occurred.
+    fn timed_out(&mut self);
+}
diff --git a/remoteauth/remote_auth_protocol/src/remote_auth_service.rs b/remoteauth/remote_auth_protocol/src/remote_auth_service.rs
new file mode 100644
index 0000000..5da33c7
--- /dev/null
+++ b/remoteauth/remote_auth_protocol/src/remote_auth_service.rs
@@ -0,0 +1,255 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate::{DeviceDiscoveryListener, DiscoveryPublisher, RemoteDevice};
+
+pub struct RemoteAuthService<'a, T: DeviceDiscoveryListener> {
+    listeners: Vec<&'a mut T>,
+}
+
+impl<'a, T: DeviceDiscoveryListener + PartialEq> RemoteAuthService<'a, T> {
+    pub fn new() -> RemoteAuthService<'a, T> {
+        RemoteAuthService {
+            listeners: Vec::new(),
+        }
+    }
+
+    pub fn is_remote_auth_supported() -> bool {
+        true
+    }
+}
+
+impl<'a, T: DeviceDiscoveryListener + PartialEq> DiscoveryPublisher<'a, T>
+    for RemoteAuthService<'a, T>
+{
+    fn add_listener(&mut self, listener: &'a mut T) -> i32 {
+        self.listeners.push(listener);
+        (self.listeners.len() - 1) as i32
+    }
+
+    fn remove_listener(&mut self, listener_id: i32) {
+        let listener_index = listener_id as usize;
+        if self.listeners.len() > listener_index {
+            self.listeners.remove(listener_index);
+        }
+    }
+
+    fn device_discovered(&mut self, remote_device: &RemoteDevice) {
+        for listener in self.listeners.iter_mut() {
+            listener.on_discovered(remote_device);
+        }
+    }
+
+    fn device_lost(&mut self, remote_device: &RemoteDevice) {
+        for listener in self.listeners.iter_mut() {
+            listener.on_lost(remote_device);
+        }
+    }
+
+    fn timed_out(&mut self) {
+        for listener in self.listeners.iter_mut() {
+            listener.on_timeout();
+        }
+    }
+}
+
+impl<'a, T: DeviceDiscoveryListener + PartialEq> Default for RemoteAuthService<'a, T> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[derive(PartialEq)]
+    struct TestListener {
+        pub remote_ids: Vec<i32>,
+        pub got_timeout: bool,
+    }
+
+    impl TestListener {
+        pub fn new() -> TestListener {
+            TestListener {
+                remote_ids: vec![],
+                got_timeout: false,
+            }
+        }
+    }
+
+    impl DeviceDiscoveryListener for TestListener {
+        fn on_discovered(&mut self, remote_device: &RemoteDevice) {
+            self.remote_ids.push(remote_device.id);
+        }
+
+        fn on_lost(&mut self, remote_device: &RemoteDevice) {
+            if let Some(index) = self.remote_ids.iter().position(|i| *i == remote_device.id) {
+                self.remote_ids.remove(index);
+            }
+        }
+
+        fn on_timeout(&mut self) {
+            self.got_timeout = true;
+        }
+    }
+
+    #[test]
+    fn single_listener_discover_notification() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device = RemoteDevice { id: 42 };
+
+        ras.add_listener(&mut listener);
+        ras.device_discovered(&remote_device);
+
+        assert_eq!(listener.remote_ids.len(), 1);
+        assert_eq!(listener.remote_ids[0], 42);
+    }
+
+    #[test]
+    fn all_listeners_discover_notification() {
+        let mut listener1 = TestListener::new();
+        let mut listener2 = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device = RemoteDevice { id: 42 };
+
+        ras.add_listener(&mut listener1);
+        ras.add_listener(&mut listener2);
+        ras.device_discovered(&remote_device);
+
+        assert_eq!(listener1.remote_ids.len(), 1);
+        assert_eq!(listener2.remote_ids.len(), 1);
+        assert_eq!(listener1.remote_ids[0], 42);
+        assert_eq!(listener2.remote_ids[0], 42);
+    }
+
+    #[test]
+    fn listeners_discover_notification_for_all_devices() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device1 = RemoteDevice { id: 42 };
+        let remote_device2 = RemoteDevice { id: 13 };
+
+        ras.add_listener(&mut listener);
+        ras.device_discovered(&remote_device1);
+        ras.device_discovered(&remote_device2);
+
+        assert_eq!(listener.remote_ids.len(), 2);
+        assert_eq!(listener.remote_ids[0], 42);
+        assert_eq!(listener.remote_ids[1], 13);
+    }
+
+    #[test]
+    fn single_listener_lost_notification() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device = RemoteDevice { id: 42 };
+
+        ras.add_listener(&mut listener);
+        ras.device_discovered(&remote_device);
+        ras.device_lost(&remote_device);
+
+        assert_eq!(listener.remote_ids.len(), 0);
+    }
+
+    #[test]
+    fn all_listeners_lost_notification() {
+        let mut listener1 = TestListener::new();
+        let mut listener2 = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device = RemoteDevice { id: 42 };
+
+        ras.add_listener(&mut listener1);
+        ras.add_listener(&mut listener2);
+        ras.device_discovered(&remote_device);
+        ras.device_lost(&remote_device);
+
+        assert_eq!(listener1.remote_ids.len(), 0);
+        assert_eq!(listener2.remote_ids.len(), 0);
+    }
+
+    #[test]
+    fn listeners_lost_notification_for_all_devices() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device1 = RemoteDevice { id: 42 };
+        let remote_device2 = RemoteDevice { id: 13 };
+        let remote_device3 = RemoteDevice { id: 99 };
+
+        ras.add_listener(&mut listener);
+        ras.device_discovered(&remote_device1);
+        ras.device_discovered(&remote_device2);
+        ras.device_discovered(&remote_device3);
+
+        ras.device_lost(&remote_device2);
+        ras.device_lost(&remote_device3);
+
+        assert_eq!(listener.remote_ids.len(), 1);
+        assert_eq!(listener.remote_ids[0], 42);
+    }
+
+    #[test]
+    fn single_listener_timeout_notification() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+
+        ras.add_listener(&mut listener);
+        ras.timed_out();
+
+        assert!(listener.got_timeout);
+    }
+
+    #[test]
+    fn all_listeners_timeout_notification() {
+        let mut listener1 = TestListener::new();
+        let mut listener2 = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+
+        ras.add_listener(&mut listener1);
+        ras.add_listener(&mut listener2);
+        ras.timed_out();
+
+        assert!(listener1.got_timeout);
+        assert!(listener2.got_timeout);
+    }
+
+    #[test]
+    fn listener_can_be_removed() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device = RemoteDevice { id: 42 };
+
+        let id = ras.add_listener(&mut listener);
+        ras.remove_listener(id);
+        ras.device_discovered(&remote_device);
+
+        assert_eq!(listener.remote_ids.len(), 0);
+    }
+
+    #[test]
+    fn listener_can_be_removed_after_notification() {
+        let mut listener = TestListener::new();
+        let mut ras = RemoteAuthService::new();
+        let remote_device = RemoteDevice { id: 42 };
+
+        let id = ras.add_listener(&mut listener);
+        ras.device_discovered(&remote_device);
+        ras.remove_listener(id);
+        ras.device_discovered(&remote_device);
+
+        assert_eq!(listener.remote_ids.len(), 1);
+        assert_eq!(listener.remote_ids[0], 42);
+    }
+}
diff --git a/remoteauth/src/ctap_protocol.rs b/remoteauth/src/ctap_protocol.rs
new file mode 100644
index 0000000..7e35e6c
--- /dev/null
+++ b/remoteauth/src/ctap_protocol.rs
@@ -0,0 +1,29 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use cmd_runner::run_cmd_shell;
+use std::path;
+
+pub(crate) fn check_ctap_protocol(root: &path::Path) -> anyhow::Result<()> {
+    log::info!("Checking CTAP Protocol");
+    let mut ffi_dir = root.to_path_buf();
+    ffi_dir.push("ctap_protocol");
+
+    run_cmd_shell(&ffi_dir, "cargo build --quiet --release --lib")?;
+    run_cmd_shell(&ffi_dir, "cargo test --quiet -- --color=always")?;
+    run_cmd_shell(&ffi_dir, "cargo doc --quiet --no-deps")?;
+    run_cmd_shell(&ffi_dir, "cargo deny check")?;
+
+    Ok(())
+}
diff --git a/remoteauth/src/main.rs b/remoteauth/src/main.rs
new file mode 100644
index 0000000..e0851eb
--- /dev/null
+++ b/remoteauth/src/main.rs
@@ -0,0 +1,103 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+extern crate core;
+
+use clap::Parser as _;
+use cmd_runner::run_cmd_shell;
+use env_logger::Env;
+use std::{env, path};
+
+mod ctap_protocol;
+mod platform;
+mod remote_auth_protocol;
+
+fn main() -> anyhow::Result<()> {
+    env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
+    let cli: Cli = Cli::parse();
+
+    let root_dir: path::PathBuf = env::var("CARGO_MANIFEST_DIR")
+        .expect("Must be run via Cargo to establish root directory")
+        .into();
+
+    match cli.subcommand {
+        Subcommand::CheckEverything(ref options) => check_everything(&root_dir, options)?,
+        Subcommand::CheckWorkspace(ref options) => check_workspace(&root_dir, options)?,
+        Subcommand::CheckCtapProtocol => ctap_protocol::check_ctap_protocol(&root_dir)?,
+        Subcommand::CheckPlatform => platform::check_platform(&root_dir)?,
+        Subcommand::CheckRemoteAuthProtocol => {
+            remote_auth_protocol::check_remote_auth_protocol(&root_dir)?
+        }
+    }
+
+    Ok(())
+}
+
+pub fn check_everything(root: &path::Path, check_options: &CheckOptions) -> anyhow::Result<()> {
+    check_workspace(root, check_options)?;
+    ctap_protocol::check_ctap_protocol(root)?;
+    platform::check_platform(root)?;
+    remote_auth_protocol::check_remote_auth_protocol(root)?;
+    Ok(())
+}
+
+pub fn check_workspace(root: &path::Path, options: &CheckOptions) -> anyhow::Result<()> {
+    log::info!("Running cargo checks on workspace");
+
+    let fmt_command = if options.reformat {
+        "cargo fmt"
+    } else {
+        "cargo fmt --check"
+    };
+
+    for cargo_cmd in [
+        fmt_command,
+        "cargo check --workspace --all-targets --quiet",
+        "cargo test --workspace --quiet -- --color=always",
+        "cargo doc --quiet --no-deps",
+        "cargo deny --workspace check",
+        "cargo clippy --all-targets --workspace -- --deny warnings",
+    ] {
+        run_cmd_shell(root, cargo_cmd)?;
+    }
+
+    Ok(())
+}
+
+#[derive(clap::Parser)]
+struct Cli {
+    #[clap(subcommand)]
+    subcommand: Subcommand,
+}
+
+#[derive(clap::Subcommand, Debug, Clone)]
+#[allow(clippy::enum_variant_names)]
+enum Subcommand {
+    /// Checks everything in remoteauth
+    CheckEverything(CheckOptions),
+    /// Checks everything included in the top level workspace
+    CheckWorkspace(CheckOptions),
+    /// Build and run tests for the CTAP Protocol
+    CheckCtapProtocol,
+    /// Builds and run tests for the Platform
+    CheckPlatform,
+    /// Builds and run tests for the Remote Auth Protocol
+    CheckRemoteAuthProtocol,
+}
+
+#[derive(clap::Args, Debug, Clone, Default)]
+pub struct CheckOptions {
+    #[arg(long, help = "reformat files with cargo fmt")]
+    reformat: bool,
+}
diff --git a/remoteauth/src/platform.rs b/remoteauth/src/platform.rs
new file mode 100644
index 0000000..481b4f7
--- /dev/null
+++ b/remoteauth/src/platform.rs
@@ -0,0 +1,29 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use cmd_runner::run_cmd_shell;
+use std::path;
+
+pub(crate) fn check_platform(root: &path::Path) -> anyhow::Result<()> {
+    log::info!("Checking Platform");
+    let mut ffi_dir = root.to_path_buf();
+    ffi_dir.push("platform");
+
+    run_cmd_shell(&ffi_dir, "cargo build --quiet --release --lib")?;
+    run_cmd_shell(&ffi_dir, "cargo test --quiet -- --color=always")?;
+    run_cmd_shell(&ffi_dir, "cargo doc --quiet --no-deps")?;
+    run_cmd_shell(&ffi_dir, "cargo deny check")?;
+
+    Ok(())
+}
diff --git a/remoteauth/src/remote_auth_protocol.rs b/remoteauth/src/remote_auth_protocol.rs
new file mode 100644
index 0000000..e47517d
--- /dev/null
+++ b/remoteauth/src/remote_auth_protocol.rs
@@ -0,0 +1,29 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use cmd_runner::run_cmd_shell;
+use std::path;
+
+pub(crate) fn check_remote_auth_protocol(root: &path::Path) -> anyhow::Result<()> {
+    log::info!("Checking Remote Auth Protocol");
+    let mut ffi_dir = root.to_path_buf();
+    ffi_dir.push("remote_auth_protocol");
+
+    run_cmd_shell(&ffi_dir, "cargo build --quiet --release --lib")?;
+    run_cmd_shell(&ffi_dir, "cargo test --quiet -- --color=always")?;
+    run_cmd_shell(&ffi_dir, "cargo doc --quiet --no-deps")?;
+    run_cmd_shell(&ffi_dir, "cargo deny check")?;
+
+    Ok(())
+}