Import 'mls-rs-core' crate

Request Document: go/android-rust-importing-crates
For CL Reviewers: go/android3p#cl-review
For Build Team: go/ab-third-party-imports
Bug: http://b/328421156
Test: m libmls_rs_core

Change-Id: I6e8a4ce28941b48f5439a0c72043fdf8c64867ba
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json
new file mode 100644
index 0000000..2c26ae7
--- /dev/null
+++ b/.cargo_vcs_info.json
@@ -0,0 +1,6 @@
+{
+  "git": {
+    "sha1": "105faf3f0a4270ef4381d82fe0f88f1d396e67e1"
+  },
+  "path_in_vcs": "mls-rs-core"
+}
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..7829b4c
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,32 @@
+// This file is generated by cargo_embargo.
+// Do not modify this file as changes will be overridden on upgrade.
+
+// TODO: Add license.
+rust_library {
+    name: "libmls_rs_core",
+    host_supported: true,
+    crate_name: "mls_rs_core",
+    cargo_env_compat: true,
+    cargo_pkg_version: "0.17.1",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    features: [
+        "default",
+        "fast_serialize",
+        "rfc_compliant",
+        "std",
+        "x509",
+    ],
+    rustlibs: [
+        "libmls_rs_codec",
+        "libthiserror",
+        "libzeroize",
+    ],
+    proc_macros: ["libmaybe_async"],
+    apex_available: [
+        "//apex_available:platform",
+        "//apex_available:anyapex",
+    ],
+    product_available: true,
+    vendor_available: true,
+}
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..73020dc
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,665 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
+[[package]]
+name = "async-trait"
+version = "0.1.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3b1be7772ee4501dba05acbe66bb1e8760f6a6c474a36035631638e4415f130"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "critical-section"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
+
+[[package]]
+name = "darling"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "either"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+
+[[package]]
+name = "ext-trait"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5"
+dependencies = [
+ "ext-trait-proc_macros",
+]
+
+[[package]]
+name = "ext-trait-proc_macros"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "extension-traits"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537"
+dependencies = [
+ "ext-trait",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "macro_rules_attribute"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862"
+dependencies = [
+ "macro_rules_attribute-proc_macro",
+ "paste",
+]
+
+[[package]]
+name = "macro_rules_attribute-proc_macro"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d"
+
+[[package]]
+name = "maybe-async"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "mls-rs-codec"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35855223b5af2d2d6f987d418f1b658d518c79a73a95664ec9357dd318f64562"
+dependencies = [
+ "mls-rs-codec-derive",
+ "thiserror",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "mls-rs-codec-derive"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f3fbf25b68713ec768c250f2b450c93ed997a5d2127b255d547ea7c6b21872b"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "mls-rs-core"
+version = "0.17.1"
+dependencies = [
+ "arbitrary",
+ "assert_matches",
+ "async-trait",
+ "hex",
+ "itertools",
+ "maybe-async",
+ "mls-rs-codec",
+ "safer-ffi",
+ "safer-ffi-gen",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-test",
+ "zeroize",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+dependencies = [
+ "critical-section",
+ "portable-atomic",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "portable-atomic"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[package]]
+name = "safer-ffi"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4483c5ab47f222d2c297e73a520c9003e09e2fe1f1b04edcb572e6939f303003"
+dependencies = [
+ "libc",
+ "macro_rules_attribute",
+ "paste",
+ "safer_ffi-proc_macros",
+ "scopeguard",
+ "uninit",
+ "unwind_safe",
+ "with_builtin_macros",
+]
+
+[[package]]
+name = "safer-ffi-gen"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdc3e72a8e99de537461ab5e9331d32c08dd558493e844cb70753a9890fdbc48"
+dependencies = [
+ "once_cell",
+ "safer-ffi",
+ "safer-ffi-gen-macro",
+]
+
+[[package]]
+name = "safer-ffi-gen-macro"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "836aa8cd7b269dcdd3d81cca1ddc136aa1d2b05f30b6a34c0ff075152b2e3771"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+ "thiserror",
+]
+
+[[package]]
+name = "safer_ffi-proc_macros"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf04ebd3786110e64269a74eea58c5564dd92a1e790c0f6f9871d6fe1b8e34db"
+dependencies = [
+ "macro_rules_attribute",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "uninit"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e130f2ed46ca5d8ec13c7ff95836827f92f5f5f37fd2b2bf16f33c408d98bb6"
+dependencies = [
+ "extension-traits",
+]
+
+[[package]]
+name = "unwind_safe"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
+
+[[package]]
+name = "wasm-bindgen-test"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "143ddeb4f833e2ed0d252e618986e18bfc7b0e52f2d28d77d05b2f045dd8eb61"
+dependencies = [
+ "console_error_panic_hook",
+ "js-sys",
+ "scoped-tls",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-bindgen-test-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-test-macro"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "with_builtin_macros"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a59d55032495429b87f9d69954c6c8602e4d3f3e0a747a12dea6b0b23de685da"
+dependencies = [
+ "with_builtin_macros-proc_macros",
+]
+
+[[package]]
+name = "with_builtin_macros-proc_macros"
+version = "0.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15bd7679c15e22924f53aee34d4e448c45b674feb6129689af88593e129f8f42"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.50",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..bb46a82
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,127 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+name = "mls-rs-core"
+version = "0.17.1"
+exclude = ["test_data"]
+description = "Core components and traits for mls-rs"
+homepage = "https://github.com/awslabs/mls-rs"
+keywords = [
+    "mls",
+    "mls-rs",
+]
+license = "Apache-2.0 OR MIT"
+repository = "https://github.com/awslabs/mls-rs"
+
+[dependencies.arbitrary]
+version = "1"
+features = ["derive"]
+optional = true
+
+[dependencies.hex]
+version = "^0.4.3"
+features = [
+    "serde",
+    "alloc",
+]
+optional = true
+default-features = false
+
+[dependencies.itertools]
+version = "0.12"
+optional = true
+
+[dependencies.maybe-async]
+version = "0.2.7"
+
+[dependencies.mls-rs-codec]
+version = "0.5.0"
+default-features = false
+
+[dependencies.safer-ffi]
+version = "0.1.3"
+optional = true
+default-features = false
+
+[dependencies.safer-ffi-gen]
+version = "0.9.2"
+optional = true
+default-features = false
+
+[dependencies.serde]
+version = "1.0"
+features = [
+    "alloc",
+    "derive",
+]
+optional = true
+default-features = false
+
+[dependencies.serde_json]
+version = "^1.0"
+optional = true
+
+[dependencies.thiserror]
+version = "1.0.40"
+optional = true
+
+[dependencies.zeroize]
+version = "1"
+features = [
+    "alloc",
+    "zeroize_derive",
+]
+default-features = false
+
+[dev-dependencies.assert_matches]
+version = "1.5.0"
+
+[features]
+arbitrary = [
+    "std",
+    "dep:arbitrary",
+]
+default = [
+    "std",
+    "rfc_compliant",
+    "fast_serialize",
+]
+fast_serialize = ["mls-rs-codec/preallocate"]
+ffi = [
+    "dep:safer-ffi",
+    "dep:safer-ffi-gen",
+]
+rfc_compliant = ["x509"]
+std = [
+    "mls-rs-codec/std",
+    "zeroize/std",
+    "safer-ffi-gen?/std",
+    "dep:thiserror",
+]
+test_suite = [
+    "dep:serde",
+    "dep:serde_json",
+    "dep:hex",
+    "dep:itertools",
+]
+x509 = []
+
+[target."cfg(mls_build_async)".dependencies.async-trait]
+version = "0.1.74"
+
+[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
+version = "^0.2.79"
+
+[target."cfg(target_arch = \"wasm32\")".dev-dependencies.wasm-bindgen-test]
+version = "0.3.26"
+default-features = false
diff --git a/LICENSE b/LICENSE
new file mode 120000
index 0000000..4ce7dad
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+LICENSE-apache
\ No newline at end of file
diff --git a/LICENSE-apache b/LICENSE-apache
new file mode 100644
index 0000000..831fbc5
--- /dev/null
+++ b/LICENSE-apache
@@ -0,0 +1,176 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, orother modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/LICENSE-mit b/LICENSE-mit
new file mode 100644
index 0000000..e547c4a
--- /dev/null
+++ b/LICENSE-mit
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including  without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to  the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN  NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..b838d1b
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,21 @@
+name: "mls-rs-core"
+description: "Core components and traits for mls-rs"
+third_party {
+  identifier {
+    type: "crates.io"
+    value: "mls-rs-core"
+  }
+  identifier {
+    type: "Archive"
+    value: "https://static.crates.io/crates/mls-rs-core/mls-rs-core-0.17.1.crate"
+    primary_source: true
+  }
+  version: "0.17.1"
+  # Dual-licensed, using the least restrictive per go/thirdpartylicenses#same.
+  license_type: NOTICE
+  last_upgrade_date {
+    year: 2024
+    month: 3
+    day: 22
+  }
+}
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..48bea6e
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 688011
+include platform/prebuilts/rust:main:/OWNERS
diff --git a/cargo_embargo.json b/cargo_embargo.json
new file mode 100644
index 0000000..cb908d7
--- /dev/null
+++ b/cargo_embargo.json
@@ -0,0 +1,3 @@
+{
+  "run_cargo": false
+}
diff --git a/src/crypto.rs b/src/crypto.rs
new file mode 100644
index 0000000..4594eeb
--- /dev/null
+++ b/src/crypto.rs
@@ -0,0 +1,439 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use crate::error::IntoAnyError;
+use alloc::vec;
+use alloc::vec::Vec;
+use core::ops::Deref;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+use zeroize::{ZeroizeOnDrop, Zeroizing};
+
+mod cipher_suite;
+pub use self::cipher_suite::*;
+
+#[cfg(feature = "test_suite")]
+pub mod test_suite;
+
+#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+/// Ciphertext produced by [`CipherSuiteProvider::hpke_seal`]
+pub struct HpkeCiphertext {
+    #[mls_codec(with = "mls_rs_codec::byte_vec")]
+    pub kem_output: Vec<u8>,
+    #[mls_codec(with = "mls_rs_codec::byte_vec")]
+    pub ciphertext: Vec<u8>,
+}
+
+/// Byte representation of an HPKE public key. For ciphersuites using elliptic curves,
+/// the public key should be represented in the uncompressed format.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, MlsSize, MlsDecode, MlsEncode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+pub struct HpkePublicKey(#[mls_codec(with = "mls_rs_codec::byte_vec")] Vec<u8>);
+
+impl From<Vec<u8>> for HpkePublicKey {
+    fn from(data: Vec<u8>) -> Self {
+        Self(data)
+    }
+}
+
+impl From<HpkePublicKey> for Vec<u8> {
+    fn from(data: HpkePublicKey) -> Self {
+        data.0
+    }
+}
+
+impl Deref for HpkePublicKey {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl AsRef<[u8]> for HpkePublicKey {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+/// Byte representation of an HPKE secret key.
+#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode, ZeroizeOnDrop)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+pub struct HpkeSecretKey(#[mls_codec(with = "mls_rs_codec::byte_vec")] Vec<u8>);
+
+impl From<Vec<u8>> for HpkeSecretKey {
+    fn from(data: Vec<u8>) -> Self {
+        Self(data)
+    }
+}
+
+impl Deref for HpkeSecretKey {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl AsRef<[u8]> for HpkeSecretKey {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+/// The HPKE context for sender outputted by [hpke_setup_s](CipherSuiteProvider::hpke_setup_s).
+/// The context internally stores the secrets generated by [hpke_setup_s](CipherSuiteProvider::hpke_setup_s).
+///
+/// This trait corresponds to ContextS from RFC 9180.
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(
+    all(not(target_arch = "wasm32"), mls_build_async),
+    maybe_async::must_be_async
+)]
+pub trait HpkeContextS {
+    type Error: IntoAnyError;
+
+    /// Encrypt `data` using the cipher key of the context with optional `aad`.
+    /// This function should internally increment the sequence number.
+    async fn seal(&mut self, aad: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>, Self::Error>;
+
+    /// Export a secret from the context for the given `exporter_context`.
+    async fn export(&self, exporter_context: &[u8], len: usize) -> Result<Vec<u8>, Self::Error>;
+}
+
+/// The HPKE context for receiver outputted by [hpke_setup_r](CipherSuiteProvider::hpke_setup_r).
+/// The context internally stores secrets received from the sender by [hpke_setup_r](CipherSuiteProvider::hpke_setup_r).
+///
+/// This trait corresponds to ContextR from RFC 9180.
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(
+    all(not(target_arch = "wasm32"), mls_build_async),
+    maybe_async::must_be_async
+)]
+pub trait HpkeContextR {
+    type Error: IntoAnyError;
+
+    /// Decrypt `ciphertext` using the cipher key of the context with optional `aad`.
+    /// This function should internally increment the sequence number.
+    async fn open(&mut self, aad: Option<&[u8]>, ciphertext: &[u8])
+        -> Result<Vec<u8>, Self::Error>;
+
+    /// Export a secret from the context for the given `exporter_context`.
+    async fn export(&self, exporter_context: &[u8], len: usize) -> Result<Vec<u8>, Self::Error>;
+}
+
+/// Byte representation of a signature public key. For ciphersuites using elliptic curves,
+/// the public key should be represented in the uncompressed format.
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::ffi_type(opaque))]
+pub struct SignaturePublicKey(#[mls_codec(with = "mls_rs_codec::byte_vec")] Vec<u8>);
+
+#[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen)]
+impl SignaturePublicKey {
+    pub fn new(bytes: Vec<u8>) -> Self {
+        bytes.into()
+    }
+
+    pub fn new_slice(data: &[u8]) -> Self {
+        Self(data.to_vec())
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl Deref for SignaturePublicKey {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl AsRef<[u8]> for SignaturePublicKey {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl From<Vec<u8>> for SignaturePublicKey {
+    fn from(data: Vec<u8>) -> Self {
+        SignaturePublicKey(data)
+    }
+}
+
+/// Byte representation of a signature key.
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    ::safer_ffi_gen::ffi_type(clone, opaque)
+)]
+#[derive(Clone, Debug, PartialEq, Eq, ZeroizeOnDrop, MlsSize, MlsEncode, MlsDecode)]
+pub struct SignatureSecretKey {
+    #[mls_codec(with = "mls_rs_codec::byte_vec")]
+    bytes: Vec<u8>,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen)]
+impl SignatureSecretKey {
+    pub fn new(bytes: Vec<u8>) -> Self {
+        bytes.into()
+    }
+
+    pub fn new_slice(data: &[u8]) -> Self {
+        Self {
+            bytes: data.to_vec(),
+        }
+    }
+
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.bytes
+    }
+}
+
+impl From<Vec<u8>> for SignatureSecretKey {
+    fn from(bytes: Vec<u8>) -> Self {
+        Self { bytes }
+    }
+}
+
+impl Deref for SignatureSecretKey {
+    type Target = Vec<u8>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.bytes
+    }
+}
+
+impl AsRef<[u8]> for SignatureSecretKey {
+    fn as_ref(&self) -> &[u8] {
+        &self.bytes
+    }
+}
+
+/// Provides implementations for several ciphersuites via [`CipherSuiteProvider`].
+pub trait CryptoProvider: Send + Sync {
+    type CipherSuiteProvider: CipherSuiteProvider + Clone;
+
+    /// Return the list of all supported ciphersuites.
+    fn supported_cipher_suites(&self) -> Vec<CipherSuite>;
+
+    /// Generate a [CipherSuiteProvider] for the given `cipher_suite`.
+    fn cipher_suite_provider(&self, cipher_suite: CipherSuite)
+        -> Option<Self::CipherSuiteProvider>;
+}
+
+/// Provides all cryptographic operations required by MLS for a given cipher suite.
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))]
+#[cfg_attr(
+    all(not(target_arch = "wasm32"), mls_build_async),
+    maybe_async::must_be_async
+)]
+pub trait CipherSuiteProvider: Send + Sync {
+    type Error: IntoAnyError;
+
+    type HpkeContextS: HpkeContextS + Send + Sync;
+    type HpkeContextR: HpkeContextR + Send + Sync;
+
+    /// Return the implemented MLS [CipherSuite](CipherSuite).
+    fn cipher_suite(&self) -> CipherSuite;
+
+    /// Compute the hash of `data`.
+    async fn hash(&self, data: &[u8]) -> Result<Vec<u8>, Self::Error>;
+
+    /// Compute the MAC tag of `data` using the `key` of length [kdf_extract_size](CipherSuiteProvider::kdf_extract_size).
+    /// Verifying a MAC tag of `data` using `key` is done by calling this function
+    /// and checking that the result matches the tag.
+    async fn mac(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Self::Error>;
+
+    /// Encrypt `data` with public additional authenticated data `aad`, using additional `nonce`
+    /// (sometimes called the initialization vector, IV). The output should include
+    /// the authentication tag, if used by the given AEAD implementation (for example,
+    /// the tag can be appended to the ciphertext).
+    async fn aead_seal(
+        &self,
+        key: &[u8],
+        data: &[u8],
+        aad: Option<&[u8]>,
+        nonce: &[u8],
+    ) -> Result<Vec<u8>, Self::Error>;
+
+    /// Decrypt the `ciphertext` generated by [aead_seal](CipherSuiteProvider::aead_seal).
+    /// This function should return an error if any of the inputs `key`, `aad` or `nonce` does not match
+    /// the corresponding input passed to [aead_seal](CipherSuiteProvider::aead_seal) to generate `ciphertext`.
+    async fn aead_open(
+        &self,
+        key: &[u8],
+        ciphertext: &[u8],
+        aad: Option<&[u8]>,
+        nonce: &[u8],
+    ) -> Result<Zeroizing<Vec<u8>>, Self::Error>;
+
+    /// Return the length of the secret key `key` passed to [aead_seal](CipherSuiteProvider::aead_seal)
+    /// and [aead_open](CipherSuiteProvider::aead_open).
+    fn aead_key_size(&self) -> usize;
+
+    /// Return the length of the `nonce` passed to [aead_seal](CipherSuiteProvider::aead_seal)
+    /// and [aead_open](CipherSuiteProvider::aead_open).
+    fn aead_nonce_size(&self) -> usize;
+
+    /// Generate a pseudo-random key `prk` extracted from the initial key
+    /// material `ikm`, using an optional random `salt`. The outputted `prk` should have
+    /// [kdf_extract_size](CipherSuiteProvider::kdf_extract_size) bytes. It can be used
+    /// as input to [kdf_expand](CipherSuiteProvider::kdf_expand).
+    ///
+    /// This function corresponds to the HKDF-Extract function from RFC 5869.
+    async fn kdf_extract(&self, salt: &[u8], ikm: &[u8])
+        -> Result<Zeroizing<Vec<u8>>, Self::Error>;
+
+    /// Generate key material of the desired length `len` by expanding the given pseudo-random key
+    /// `prk` of length [kdf_extract_size](CipherSuiteProvider::kdf_extract_size).
+    /// The additional input `info` contains optional context data.
+    ///
+    /// This function corresponds to the HKDF-Expand function from RFC 5869.
+    async fn kdf_expand(
+        &self,
+        prk: &[u8],
+        info: &[u8],
+        len: usize,
+    ) -> Result<Zeroizing<Vec<u8>>, Self::Error>;
+
+    /// Return the size of pseudo-random key `prk` outputted by [kdf_extract](CipherSuiteProvider::kdf_extract)
+    /// and inputted to [kdf_expand](CipherSuiteProvider::kdf_expand).
+    fn kdf_extract_size(&self) -> usize;
+
+    /// Encrypt the plaintext `pt` with optional public additional authenticated data `aad` to the
+    /// public key `remote_key` using additional context information `info` (which can be empty if
+    /// not needed). This function combines the action
+    /// of the [hpke_setup_s](CipherSuiteProvider::hpke_setup_s) and then calling [seal](HpkeContextS::seal)
+    /// on the resulting [HpkeContextS](self::HpkeContextS).
+    ///
+    /// This function corresponds to the one-shot API in base mode in RFC 9180.
+    async fn hpke_seal(
+        &self,
+        remote_key: &HpkePublicKey,
+        info: &[u8],
+        aad: Option<&[u8]>,
+        pt: &[u8],
+    ) -> Result<HpkeCiphertext, Self::Error>;
+
+    /// Decrypt the `ciphertext` generated by [hpke_seal](CipherSuiteProvider::hpke_seal).
+    /// This function combines the action of the [hpke_setup_r](CipherSuiteProvider::hpke_setup_r)
+    /// and then calling [open](HpkeContextR::open) on the resulting [HpkeContextR](self::HpkeContextR).
+    ///
+    /// This function corresponds to the one-shot API in base mode in RFC 9180.
+    async fn hpke_open(
+        &self,
+        ciphertext: &HpkeCiphertext,
+        local_secret: &HpkeSecretKey,
+        local_public: &HpkePublicKey,
+        info: &[u8],
+        aad: Option<&[u8]>,
+    ) -> Result<Vec<u8>, Self::Error>;
+
+    /// Generate a tuple containing the ciphertext `kem_output` that can
+    /// be used as the input to [hpke_setup_r](CipherSuiteProvider::hpke_setup_r),
+    /// as well as the sender context [HpkeContextS](self::HpkeContextS) that can be
+    /// used to generate AEAD ciphertexts and export keys.
+    ///
+    /// The inputted `remote_key` will normally be generated using
+    /// [kem_derive](CipherSuiteProvider::kem_derive) or
+    /// [kem_generate](CipherSuiteProvider::kem_generate). However, the function
+    /// should return an error if the format is incorrect.
+    ///
+    /// This function corresponds to the SetupBaseS function from RFC 9180.
+    async fn hpke_setup_s(
+        &self,
+        remote_key: &HpkePublicKey,
+        info: &[u8],
+    ) -> Result<(Vec<u8>, Self::HpkeContextS), Self::Error>;
+
+    /// Receive the ciphertext `kem_output` generated by [hpke_setup_s](CipherSuiteProvider::hpke_setup_s)
+    /// and the `local_secret` corresponding to the `remote_key` used as input to
+    /// [hpke_setup_s](CipherSuiteProvider::hpke_setup_s). The ouput is the receiver context
+    /// [HpkeContextR](self::HpkeContextR) that can be used to decrypt AEAD ciphertexts
+    /// generated by the sender context [HpkeContextS](self::HpkeContextS) outputted by
+    /// [hpke_setup_r](CipherSuiteProvider::hpke_setup_r)
+    /// and export the same keys as that context.
+    ///
+    /// The inputted `local_secret` will normally be generated using
+    /// [kem_derive](CipherSuiteProvider::kem_derive) or
+    /// [kem_generate](CipherSuiteProvider::kem_generate). However, the function
+    /// should return an error if the format is incorrect.
+    ///
+    /// This function corresponds to the SetupBaseR function from RFC 9180.
+    async fn hpke_setup_r(
+        &self,
+        kem_output: &[u8],
+        local_secret: &HpkeSecretKey,
+        local_public: &HpkePublicKey,
+
+        info: &[u8],
+    ) -> Result<Self::HpkeContextR, Self::Error>;
+
+    /// Derive from the initial key material `ikm` the KEM keys used as inputs to
+    /// [hpke_setup_r](CipherSuiteProvider::hpke_setup_r),
+    /// [hpke_setup_s](CipherSuiteProvider::hpke_setup_s), [hpke_seal](CipherSuiteProvider::hpke_seal)
+    /// and [hpke_open](CipherSuiteProvider::hpke_open).
+    async fn kem_derive(&self, ikm: &[u8]) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error>;
+
+    /// Generate fresh KEM keys to be used as inputs to [hpke_setup_r](CipherSuiteProvider::hpke_setup_r),
+    /// [hpke_setup_s](CipherSuiteProvider::hpke_setup_s), [hpke_seal](CipherSuiteProvider::hpke_seal)
+    /// and [hpke_open](CipherSuiteProvider::hpke_open).
+    async fn kem_generate(&self) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error>;
+
+    /// Verify that the given byte vector `key` can be decoded as an HPKE public key.
+    fn kem_public_key_validate(&self, key: &HpkePublicKey) -> Result<(), Self::Error>;
+
+    /// Fill `out` with random bytes.
+    fn random_bytes(&self, out: &mut [u8]) -> Result<(), Self::Error>;
+
+    /// Generate `count` bytes of pseudorandom bytes as a vector. This is a shortcut for
+    /// creating a `Vec<u8>` of `count` bytes and calling [random_bytes](CipherSuiteProvider::random_bytes).
+    fn random_bytes_vec(&self, count: usize) -> Result<Vec<u8>, Self::Error> {
+        let mut vec = vec![0u8; count];
+        self.random_bytes(&mut vec)?;
+
+        Ok(vec)
+    }
+
+    /// Generate fresh signature keys to be used as inputs to [sign](CipherSuiteProvider::sign)
+    /// and [verify](CipherSuiteProvider::verify)
+    async fn signature_key_generate(
+        &self,
+    ) -> Result<(SignatureSecretKey, SignaturePublicKey), Self::Error>;
+
+    /// Output a public key corresponding to `secret_key`.
+    async fn signature_key_derive_public(
+        &self,
+        secret_key: &SignatureSecretKey,
+    ) -> Result<SignaturePublicKey, Self::Error>;
+
+    /// Sign `data` using `secret_key`.
+    async fn sign(
+        &self,
+        secret_key: &SignatureSecretKey,
+        data: &[u8],
+    ) -> Result<Vec<u8>, Self::Error>;
+
+    /// Verify that the secret key corresponding to `public_key` created the `signature` over `data`.
+    async fn verify(
+        &self,
+        public_key: &SignaturePublicKey,
+        signature: &[u8],
+        data: &[u8],
+    ) -> Result<(), Self::Error>;
+}
diff --git a/src/crypto/cipher_suite.rs b/src/crypto/cipher_suite.rs
new file mode 100644
index 0000000..e87e10b
--- /dev/null
+++ b/src/crypto/cipher_suite.rs
@@ -0,0 +1,97 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::ops::Deref;
+
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+/// Wrapper type representing a ciphersuite identifier
+/// along with default values defined by the MLS RFC. Custom ciphersuites
+/// can be defined using a custom [`CryptoProvider`](crate::crypto::CryptoProvider).
+///
+/// ## Default Ciphersuites
+///
+/// Note: KEM values are defined by the HPKE standard (RFC 9180).
+///
+/// |    |             |         |         |                  |
+/// |----|-------------|---------|---------|------------------|
+/// | ID | KEM         | AEAD  | Hash Function    | Signature Scheme |
+/// | 1  | DHKEMX25519 | AES 128 | SHA 256 | Ed25519          |
+/// | 2  | DHKEMP256   | AES 128 | SHA 256 | P256             |
+/// | 3  | DHKEMX25519 | ChaCha20Poly1305 | SHA 256 | Ed25519 |
+/// | 4  | DHKEMX448   | AES 256 | SHA 512 | Ed448            |
+/// | 5  | DHKEMP521   | AES 256 | SHA 512 | P521             |
+/// | 6  | DHKEMX448   | ChaCha20Poly1305 | SHA 512 | Ed448   |
+/// | 7  | DHKEMP384   | AES 256 | SHA 512 | P384             |
+#[derive(Debug, Copy, Clone, Eq, PartialEq, MlsSize, MlsEncode, MlsDecode, PartialOrd, Ord)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
+#[repr(transparent)]
+pub struct CipherSuite(u16);
+
+impl From<u16> for CipherSuite {
+    fn from(value: u16) -> Self {
+        CipherSuite(value)
+    }
+}
+
+impl From<CipherSuite> for u16 {
+    fn from(val: CipherSuite) -> Self {
+        val.0
+    }
+}
+
+impl Deref for CipherSuite {
+    type Target = u16;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl CipherSuite {
+    /// MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519
+    pub const CURVE25519_AES128: CipherSuite = CipherSuite(1);
+    /// MLS_128_DHKEMP256_AES128GCM_SHA256_P256
+    pub const P256_AES128: CipherSuite = CipherSuite(2);
+    /// MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519
+    pub const CURVE25519_CHACHA: CipherSuite = CipherSuite(3);
+    /// MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448
+    pub const CURVE448_AES256: CipherSuite = CipherSuite(4);
+    /// MLS_256_DHKEMP521_AES256GCM_SHA512_P521
+    pub const P521_AES256: CipherSuite = CipherSuite(5);
+    /// MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448
+    pub const CURVE448_CHACHA: CipherSuite = CipherSuite(6);
+    /// MLS_256_DHKEMP384_AES256GCM_SHA384_P384
+    pub const P384_AES256: CipherSuite = CipherSuite(7);
+
+    /// Ciphersuite from a raw value.
+    pub const fn new(value: u16) -> CipherSuite {
+        CipherSuite(value)
+    }
+
+    /// Raw numerical value wrapped value.
+    pub const fn raw_value(&self) -> u16 {
+        self.0
+    }
+
+    /// An iterator over all of the default MLS ciphersuites.
+    pub fn all() -> impl Iterator<Item = CipherSuite> {
+        (1..=7).map(CipherSuite)
+    }
+}
+
+/// Modes of HPKE operation.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[repr(u8)]
+pub enum HpkeModeId {
+    /// Base mode of HPKE for key exchange and AEAD cipher
+    Base = 0x00,
+    /// Base mode with a user provided PSK
+    Psk = 0x01,
+    /// Authenticated variant that authenticates possession of a KEM private key.
+    Auth = 0x02,
+    /// Authenticated variant that authenticates possession of a PSK as well as a KEM private key.
+    AuthPsk = 0x03,
+}
diff --git a/src/crypto/test_suite.rs b/src/crypto/test_suite.rs
new file mode 100644
index 0000000..44430e7
--- /dev/null
+++ b/src/crypto/test_suite.rs
@@ -0,0 +1,687 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use alloc::vec::Vec;
+use itertools::Itertools;
+
+use crate::crypto::HpkeContextR;
+
+use super::{
+    CipherSuiteProvider, CryptoProvider, HpkeCiphertext, HpkeContextS, HpkePublicKey, HpkeSecretKey,
+};
+
+const PATH: &str = concat!(
+    env!("CARGO_MANIFEST_DIR"),
+    "/test_data/crypto_provider.json"
+);
+
+#[cfg(any(target_arch = "wasm32", not(feature = "std")))]
+const SERIALIZED_TEST_SUITES: &[u8] = include_bytes!(concat!(
+    env!("CARGO_MANIFEST_DIR"),
+    "/test_data/crypto_provider.json"
+));
+
+pub use hpke_rfc_conformance::{
+    verify_hpke_context_tests, verify_hpke_encap_tests, EncapOutput, TestHpke,
+};
+
+pub const DATA_SIZES: [usize; 5] = [0, 1, 16, 123, 2000];
+
+#[derive(serde::Serialize, serde::Deserialize, Default)]
+struct TestSuite {
+    cipher_suite: u16,
+    #[serde(default)]
+    signature_tests: Vec<SignatureTestCase>,
+    #[serde(default)]
+    aead_tests: Vec<AeadTestCase>,
+    #[serde(default)]
+    hpke_tests: HpkeTestCases,
+    #[serde(default)]
+    hkdf_tests: Vec<HkdfTestCase>,
+    #[serde(default)]
+    mac_tests: Vec<MacTestCase>,
+    #[serde(default)]
+    hash_tests: Vec<HashTestCase>,
+}
+
+#[cfg(all(not(mls_build_async), not(target_arch = "wasm32"), feature = "std"))]
+#[cfg_attr(coverage_nightly, coverage(off))]
+pub fn generate_tests<C: CryptoProvider>(crypto: &C) {
+    for cs in crypto.supported_cipher_suites() {
+        crypto.cipher_suite_provider(cs).unwrap();
+    }
+
+    let mut test_suites = create_or_load_tests(crypto);
+
+    for test_suite in test_suites.iter_mut() {
+        let cs = test_suite.cipher_suite.into();
+        let cs = crypto.cipher_suite_provider(cs).unwrap();
+
+        test_suite.signature_tests = generate_signature_tests(&cs);
+        test_suite.hpke_tests = generate_hpke_tests(&cs);
+        test_suite.hkdf_tests = generate_hkdf_tests(&cs);
+    }
+
+    std::fs::write(PATH, serde_json::to_string_pretty(&test_suites).unwrap()).unwrap();
+}
+
+#[cfg(all(not(mls_build_async), not(target_arch = "wasm32"), feature = "std"))]
+#[cfg_attr(coverage_nightly, coverage(off))]
+fn create_or_load_tests<C: CryptoProvider>(crypto: &C) -> Vec<TestSuite> {
+    if std::path::Path::new(PATH).exists() {
+        serde_json::from_slice(&std::fs::read(PATH).unwrap()).unwrap()
+    } else {
+        crypto
+            .supported_cipher_suites()
+            .into_iter()
+            .map(|cipher_suite| TestSuite {
+                cipher_suite: cipher_suite.into(),
+                ..Default::default()
+            })
+            .collect()
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+pub async fn verify_tests<C: CryptoProvider>(crypto: &C, signature_secret_key_compatible: bool) {
+    #[cfg(any(target_arch = "wasm32", not(feature = "std")))]
+    let test_suites: Vec<TestSuite> = serde_json::from_slice(SERIALIZED_TEST_SUITES).unwrap();
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
+    let test_suites: Vec<TestSuite> =
+        serde_json::from_slice(&std::fs::read(PATH).unwrap()).unwrap();
+
+    for test_suite in test_suites {
+        let test_cs = test_suite.cipher_suite.into();
+
+        let Some(cs) = crypto.cipher_suite_provider(test_cs) else {
+            continue;
+        };
+
+        assert_eq!(cs.cipher_suite(), test_cs);
+
+        verify_hkdf_tests(&cs, test_suite.hkdf_tests).await;
+        verify_aead_tests(&cs, test_suite.aead_tests).await;
+        verify_mac_tests(&cs, test_suite.mac_tests).await;
+        verify_hpke_tests(&cs, test_suite.hpke_tests).await;
+
+        verify_signature_tests(
+            &cs,
+            test_suite.signature_tests,
+            signature_secret_key_compatible,
+        )
+        .await;
+
+        verify_hash_tests(&cs, test_suite.hash_tests).await;
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct SignatureTestCase {
+    #[serde(with = "hex::serde")]
+    secret: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    public: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    data: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    signature: Vec<u8>,
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_signature_tests<C: CipherSuiteProvider>(
+    cs: &C,
+    test_cases: Vec<SignatureTestCase>,
+    secret_key_compatible: bool,
+) {
+    // Checks that `cs` can sign and verify
+    let generated = generate_signature_tests(cs).await;
+
+    for (test_case, is_generated) in test_cases
+        .into_iter()
+        .map(|tc| (tc, false))
+        .chain(generated.into_iter().map(|tc| (tc, true)))
+    {
+        let public = test_case.public.into();
+
+        // Checks that `cs` can verify signatures generated by itself and another implementation
+        cs.verify(&public, &test_case.signature, &test_case.data)
+            .await
+            .unwrap();
+
+        if is_generated || secret_key_compatible {
+            let secret = test_case.secret.into();
+
+            let derived = cs.signature_key_derive_public(&secret).await.unwrap();
+
+            cs.sign(&secret, b"hello world").await.unwrap();
+
+            assert_eq!(derived, public);
+        }
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+async fn generate_signature_tests<C: CipherSuiteProvider>(cs: &C) -> Vec<SignatureTestCase> {
+    let mut tests = Vec::new();
+
+    for data_size in DATA_SIZES {
+        let data = cs.random_bytes_vec(data_size).unwrap();
+        let (secret, public) = cs.signature_key_generate().await.unwrap();
+        let signature = cs.sign(&secret, &data).await.unwrap();
+
+        tests.push(SignatureTestCase {
+            secret: secret.to_vec(),
+            public: public.to_vec(),
+            data,
+            signature,
+        });
+    }
+
+    tests
+}
+
+// Test vectors from the RFC
+#[derive(serde::Deserialize, serde::Serialize)]
+struct AeadTestCase {
+    #[serde(with = "hex::serde")]
+    pub key: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub iv: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub ct: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub aad: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub pt: Vec<u8>,
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_aead_tests<C: CipherSuiteProvider>(cs: &C, test_cases: Vec<AeadTestCase>) {
+    for case in test_cases {
+        let ciphertext = cs
+            .aead_seal(&case.key, &case.pt, Some(&case.aad), &case.iv)
+            .await
+            .unwrap();
+
+        assert_eq!(ciphertext, case.ct);
+
+        let plaintext = cs
+            .aead_open(&case.key, &ciphertext, Some(&case.aad), &case.iv)
+            .await
+            .unwrap();
+
+        assert_eq!(plaintext.to_vec(), case.pt);
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Default)]
+struct HpkeTestCases {
+    #[serde(with = "hex::serde")]
+    ikm: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    secret: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    public: Vec<u8>,
+
+    seal_tests: Vec<HpkeSealTestCase>,
+    export_tests: Vec<HpkeExportTestCase>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct HpkeSealTestCase {
+    #[serde(with = "hex::serde")]
+    plaintext: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    info: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    aad: Vec<u8>,
+
+    // Seal and open
+    #[serde(with = "hex::serde")]
+    sealed_kem_output: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    sealed_ciphertext: Vec<u8>,
+
+    // Setup s and r
+    #[serde(with = "hex::serde")]
+    setup_s_kem_output: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    setup_s_ciphertext: Vec<u8>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+struct HpkeExportTestCase {
+    #[serde(with = "hex::serde")]
+    info: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    kem_output: Vec<u8>,
+
+    #[serde(with = "hex::serde")]
+    exporter_context: Vec<u8>,
+    exported_len: usize,
+    #[serde(with = "hex::serde")]
+    exported: Vec<u8>,
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_hpke_tests<C: CipherSuiteProvider>(cs: &C, test_cases: HpkeTestCases) {
+    let generated = generate_hpke_tests(cs).await;
+    verify_hpke_test(cs, generated).await;
+    verify_hpke_test(cs, test_cases).await;
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_hpke_test<C: CipherSuiteProvider>(cs: &C, test_cases: HpkeTestCases) {
+    let (secret, public) = cs.kem_derive(&test_cases.ikm).await.unwrap();
+
+    assert_eq!(&secret, &test_cases.secret.into());
+    assert_eq!(&public, &test_cases.public.into());
+
+    for test in test_cases.seal_tests {
+        let ct = HpkeCiphertext {
+            kem_output: test.sealed_kem_output.clone(),
+            ciphertext: test.sealed_ciphertext.clone(),
+        };
+
+        test_open_ciphertext(cs, &secret, &public, &ct, &test).await;
+
+        let ct = HpkeCiphertext {
+            kem_output: test.setup_s_kem_output.clone(),
+            ciphertext: test.setup_s_ciphertext.clone(),
+        };
+
+        test_open_ciphertext(cs, &secret, &public, &ct, &test).await;
+    }
+
+    for test in test_cases.export_tests {
+        let context_r = cs
+            .hpke_setup_r(&test.kem_output, &secret, &public, &test.info)
+            .await
+            .unwrap();
+
+        let exported = context_r
+            .export(&test.exporter_context, test.exported_len)
+            .await
+            .unwrap();
+
+        assert_eq!(exported, test.exported);
+    }
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn test_open_ciphertext<C: CipherSuiteProvider>(
+    cs: &C,
+    secret: &HpkeSecretKey,
+    public: &HpkePublicKey,
+    ct: &HpkeCiphertext,
+    test: &HpkeSealTestCase,
+) {
+    let aad = (!test.aad.is_empty()).then_some(test.aad.as_slice());
+
+    let opened = cs
+        .hpke_open(ct, secret, public, &test.info, aad)
+        .await
+        .unwrap();
+
+    assert_eq!(&opened, &test.plaintext);
+
+    let mut context_r = cs
+        .hpke_setup_r(&ct.kem_output, secret, public, &test.info)
+        .await
+        .unwrap();
+
+    let opened = context_r.open(aad, &ct.ciphertext).await.unwrap();
+    assert_eq!(&opened, &test.plaintext);
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(coverage_nightly, coverage(off))]
+async fn generate_hpke_tests<C: CipherSuiteProvider>(cs: &C) -> HpkeTestCases {
+    let ikm = cs.random_bytes_vec(cs.kdf_extract_size()).unwrap();
+    let (secret, public) = cs.kem_derive(&ikm).await.unwrap();
+
+    let sizes_iter = DATA_SIZES.iter().copied();
+
+    let mut seal_tests = Vec::new();
+
+    for ((pt_size, info_size), aad_size) in sizes_iter
+        .clone()
+        .skip(1)
+        .cartesian_product(sizes_iter.clone())
+        .cartesian_product(sizes_iter.clone())
+    {
+        let plaintext = cs.random_bytes_vec(pt_size).unwrap();
+        let info = cs.random_bytes_vec(info_size).unwrap();
+        let aad = cs.random_bytes_vec(aad_size).unwrap();
+
+        let sealed = cs
+            .hpke_seal(&public, &info, (aad_size > 0).then_some(&aad), &plaintext)
+            .await
+            .unwrap();
+
+        let (setup_s_kem_output, mut context_s) = cs.hpke_setup_s(&public, &info).await.unwrap();
+
+        let setup_s_ciphertext = context_s
+            .seal((aad_size > 0).then_some(&aad), &plaintext)
+            .await
+            .unwrap();
+
+        seal_tests.push(HpkeSealTestCase {
+            plaintext,
+            info,
+            aad,
+            sealed_kem_output: sealed.kem_output,
+            sealed_ciphertext: sealed.ciphertext,
+            setup_s_kem_output,
+            setup_s_ciphertext,
+        })
+    }
+
+    let mut export_tests = Vec::new();
+
+    for ((context_len, exported_len), info_size) in sizes_iter
+        .clone()
+        .cartesian_product(sizes_iter.clone().skip(1))
+        .cartesian_product(sizes_iter)
+    {
+        let exporter_context = cs.random_bytes_vec(context_len).unwrap();
+        let info = cs.random_bytes_vec(info_size).unwrap();
+        let (kem_output, context) = cs.hpke_setup_s(&public, &info).await.unwrap();
+
+        let exported = context
+            .export(&exporter_context, exported_len)
+            .await
+            .unwrap();
+
+        export_tests.push(HpkeExportTestCase {
+            info,
+            kem_output,
+            exporter_context,
+            exported_len,
+            exported,
+        });
+    }
+
+    HpkeTestCases {
+        ikm,
+        secret: secret.to_vec(),
+        public: public.to_vec(),
+        seal_tests,
+        export_tests,
+    }
+}
+
+#[derive(serde::Deserialize, serde::Serialize)]
+struct HkdfTestCase {
+    #[serde(with = "hex::serde")]
+    pub ikm: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub salt: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub info: Vec<u8>,
+    pub len: usize,
+    #[serde(with = "hex::serde")]
+    pub prk: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    pub okm: Vec<u8>,
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_hkdf_tests<C: CipherSuiteProvider>(cs: &C, test_cases: Vec<HkdfTestCase>) {
+    for case in test_cases {
+        let extracted = cs.kdf_extract(&case.salt, &case.ikm).await.unwrap();
+
+        assert_eq!(extracted.to_vec(), case.prk);
+
+        let expanded = cs
+            .kdf_expand(&case.prk, &case.info, case.len)
+            .await
+            .unwrap();
+
+        assert_eq!(expanded.to_vec(), case.okm);
+    }
+}
+
+#[cfg(all(not(mls_build_async), not(target_arch = "wasm32"), feature = "std"))]
+#[cfg_attr(coverage_nightly, coverage(off))]
+fn generate_hkdf_tests<C: CipherSuiteProvider>(cs: &C) -> Vec<HkdfTestCase> {
+    let iter = DATA_SIZES.iter().copied();
+
+    let iter = iter
+        .clone()
+        .skip(1)
+        .cartesian_product(iter.clone())
+        .cartesian_product(iter.clone())
+        .cartesian_product(iter.skip(1));
+
+    iter.map(|(((ikm_size, salt_size), info_size), len)| {
+        let ikm = cs.random_bytes_vec(ikm_size).unwrap();
+        let salt = cs.random_bytes_vec(salt_size).unwrap();
+        let info = cs.random_bytes_vec(info_size).unwrap();
+
+        let prk = cs.kdf_extract(&salt, &ikm).unwrap().to_vec();
+        let okm = cs.kdf_expand(&prk, &info, len).unwrap().to_vec();
+
+        HkdfTestCase {
+            ikm,
+            salt,
+            info,
+            len,
+            prk,
+            okm,
+        }
+    })
+    .collect()
+}
+
+// Test vectors from RFC 4231
+#[derive(serde::Deserialize, serde::Serialize)]
+struct MacTestCase {
+    #[serde(with = "hex::serde")]
+    key: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    data: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    tag: Vec<u8>,
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_mac_tests<C: CipherSuiteProvider>(cs: &C, test_cases: Vec<MacTestCase>) {
+    for case in test_cases {
+        let computed = cs.mac(&case.key, &case.data).await.unwrap();
+        assert_eq!(computed, case.tag);
+    }
+}
+
+#[derive(serde::Deserialize, serde::Serialize)]
+struct HashTestCase {
+    #[serde(with = "hex::serde")]
+    input: Vec<u8>,
+    #[serde(with = "hex::serde")]
+    output: Vec<u8>,
+}
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+async fn verify_hash_tests<C: CipherSuiteProvider>(cs: &C, test_cases: Vec<HashTestCase>) {
+    for case in test_cases {
+        let computed = cs.hash(&case.input).await.unwrap();
+        assert_eq!(computed, case.output);
+    }
+}
+
+mod hpke_rfc_conformance {
+    use alloc::vec::Vec;
+
+    use crate::crypto::{CipherSuite, HpkeContextR, HpkeContextS, HpkeModeId};
+
+    #[derive(serde::Deserialize, Debug, Clone)]
+    pub struct TestCaseAlgo {
+        pub kem_id: u16,
+        pub kdf_id: u16,
+        pub aead_id: u16,
+        pub mode: u8,
+    }
+
+    impl TestCaseAlgo {
+        fn cipher_suite(&self) -> Option<CipherSuite> {
+            if ![HpkeModeId::Base as u8, HpkeModeId::Psk as u8].contains(&self.mode) {
+                return None;
+            }
+
+            match (self.kem_id, self.kdf_id, self.aead_id) {
+                (0x0010, 0x0001, 0x0001) => Some(CipherSuite::P256_AES128),
+                (0x0011, 0x0002, 0x0002) => Some(CipherSuite::P384_AES256),
+                (0x0012, 0x0003, 0x0002) => Some(CipherSuite::P521_AES256),
+                (0x0020, 0x0001, 0x0001) => Some(CipherSuite::CURVE25519_AES128),
+                (0x0020, 0x0001, 0x0003) => Some(CipherSuite::CURVE25519_CHACHA),
+                (0x0021, 0x0003, 0x0002) => Some(CipherSuite::CURVE448_AES256),
+                (0x0021, 0x0003, 0x0003) => Some(CipherSuite::CURVE448_CHACHA),
+                _ => None,
+            }
+        }
+    }
+
+    #[derive(serde::Deserialize, Debug)]
+    struct TestCase {
+        #[serde(flatten)]
+        algo: TestCaseAlgo,
+        #[serde(with = "hex::serde", rename(deserialize = "pkRm"))]
+        pk_rm: Vec<u8>,
+        #[serde(with = "hex::serde", rename(deserialize = "skRm"))]
+        sk_rm: Vec<u8>,
+        #[serde(with = "hex::serde", rename(deserialize = "ikmE"))]
+        ikm_e: Vec<u8>,
+        #[serde(with = "hex::serde")]
+        shared_secret: Vec<u8>,
+        #[serde(with = "hex::serde")]
+        enc: Vec<u8>,
+        #[serde(with = "hex::serde")]
+        exporter_secret: Vec<u8>,
+        #[serde(with = "hex::serde")]
+        base_nonce: Vec<u8>,
+        #[serde(with = "hex::serde")]
+        key: Vec<u8>,
+        encryptions: Vec<EncryptionTestCase>,
+        exports: Vec<ExportTestCase>,
+    }
+
+    #[derive(serde::Deserialize, Debug)]
+    struct EncryptionTestCase {
+        #[serde(with = "hex::serde", rename = "pt")]
+        plaintext: Vec<u8>,
+        #[serde(with = "hex::serde")]
+        aad: Vec<u8>,
+        #[serde(with = "hex::serde", rename = "ct")]
+        ciphertext: Vec<u8>,
+    }
+
+    #[derive(serde::Deserialize, Debug)]
+    struct ExportTestCase {
+        #[serde(with = "hex::serde")]
+        exporter_context: Vec<u8>,
+        #[serde(rename = "L")]
+        length: usize,
+        #[serde(with = "hex::serde")]
+        exported_value: Vec<u8>,
+    }
+
+    #[cfg(any(target_arch = "wasm32", not(feature = "std")))]
+    fn get_test_cases() -> Vec<TestCase> {
+        let bytes = include_bytes!(concat!(
+            env!("CARGO_MANIFEST_DIR"),
+            "/test_data/test_hpke.json"
+        ));
+
+        serde_json::from_slice(bytes).unwrap()
+    }
+
+    #[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
+    fn get_test_cases() -> Vec<TestCase> {
+        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/test_data/test_hpke.json");
+
+        serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap()
+    }
+
+    pub struct EncapOutput {
+        pub enc: Vec<u8>,
+        pub shared_secret: Vec<u8>,
+    }
+
+    impl EncapOutput {
+        pub fn new(enc: Vec<u8>, shared_secret: Vec<u8>) -> Self {
+            Self { enc, shared_secret }
+        }
+    }
+
+    pub trait TestHpke {
+        type ContextS: HpkeContextS;
+        type ContextR: HpkeContextR;
+
+        fn hpke_context(
+            &self,
+            key: Vec<u8>,
+            base_nonce: Vec<u8>,
+            exporter_secret: Vec<u8>,
+        ) -> (Self::ContextS, Self::ContextR);
+
+        fn encap(&mut self, ikm_e: Vec<u8>, pk_rm: Vec<u8>) -> EncapOutput;
+        fn decap(&mut self, enc: Vec<u8>, sk_rm: Vec<u8>, pk_rm: Vec<u8>) -> Vec<u8>;
+    }
+
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn verify_hpke_context_tests<C: TestHpke>(hpke: &C, cipher_suite: CipherSuite) {
+        for test_case in get_test_cases()
+            .into_iter()
+            .filter(|tc| matches!(tc.algo.cipher_suite(), Some(c) if c == cipher_suite))
+        {
+            let (mut context_s, mut context_r) = hpke.hpke_context(
+                test_case.key,
+                test_case.base_nonce,
+                test_case.exporter_secret,
+            );
+
+            for enc_test_case in test_case.encryptions {
+                // Encrypt
+                let ct = context_s
+                    .seal(Some(&enc_test_case.aad), &enc_test_case.plaintext)
+                    .await
+                    .unwrap();
+
+                assert_eq!(ct, enc_test_case.ciphertext);
+
+                // Decrypt
+                let pt = context_r.open(Some(&enc_test_case.aad), &ct).await.unwrap();
+
+                assert_eq!(pt, enc_test_case.plaintext);
+            }
+
+            for test in test_case.exports {
+                let exported_s = context_s.export(&test.exporter_context, test.length).await;
+                assert_eq!(exported_s.unwrap(), test.exported_value);
+
+                let exported_r = context_r.export(&test.exporter_context, test.length).await;
+                assert_eq!(exported_r.unwrap(), test.exported_value);
+            }
+        }
+    }
+
+    #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+    pub async fn verify_hpke_encap_tests<C: TestHpke>(hpke: &mut C, cipher_suite: CipherSuite) {
+        for test_case in get_test_cases()
+            .into_iter()
+            .filter(|tc| matches!(tc.algo.cipher_suite(), Some(c) if c == cipher_suite))
+        {
+            let out = hpke.encap(test_case.ikm_e, test_case.pk_rm.clone());
+
+            assert_eq!(&out.enc, &test_case.enc);
+            assert_eq!(&out.shared_secret, &test_case.shared_secret);
+
+            let shared_secret = hpke.decap(test_case.enc, test_case.sk_rm, test_case.pk_rm);
+
+            assert_eq!(shared_secret, test_case.shared_secret);
+        }
+    }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..cf69f97
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,63 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::fmt::{self, Display};
+
+#[cfg(feature = "std")]
+#[derive(Debug)]
+/// Generic error used to wrap errors produced by providers.
+pub struct AnyError(Box<dyn std::error::Error + Send + Sync>);
+
+#[cfg(not(feature = "std"))]
+#[derive(Debug)]
+pub struct AnyError;
+
+#[cfg(feature = "std")]
+impl Display for AnyError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AnyError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        self.0.source()
+    }
+}
+
+#[cfg(not(feature = "std"))]
+impl Display for AnyError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        core::fmt::Debug::fmt(self, f)
+    }
+}
+
+/// Trait to convert a provider specific error into [`AnyError`]
+pub trait IntoAnyError: core::fmt::Debug + Sized {
+    #[cfg(feature = "std")]
+    fn into_any_error(self) -> AnyError {
+        self.into_dyn_error()
+            .map_or_else(|this| AnyError(format!("{this:?}").into()), AnyError)
+    }
+
+    #[cfg(not(feature = "std"))]
+    fn into_any_error(self) -> AnyError {
+        AnyError
+    }
+
+    #[cfg(feature = "std")]
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Err(self)
+    }
+}
+
+impl IntoAnyError for mls_rs_codec::Error {
+    #[cfg(feature = "std")]
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+impl IntoAnyError for core::convert::Infallible {}
diff --git a/src/extension.rs b/src/extension.rs
new file mode 100644
index 0000000..1f5b6b8
--- /dev/null
+++ b/src/extension.rs
@@ -0,0 +1,250 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::ops::Deref;
+
+use crate::error::{AnyError, IntoAnyError};
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+mod list;
+
+pub use list::*;
+
+/// Wrapper type representing an extension identifier along with default values
+/// defined by the MLS RFC.
+#[derive(
+    Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, MlsSize, MlsEncode, MlsDecode,
+)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
+#[repr(transparent)]
+pub struct ExtensionType(u16);
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl ExtensionType {
+    pub const APPLICATION_ID: ExtensionType = ExtensionType(1);
+    pub const RATCHET_TREE: ExtensionType = ExtensionType(2);
+    pub const REQUIRED_CAPABILITIES: ExtensionType = ExtensionType(3);
+    pub const EXTERNAL_PUB: ExtensionType = ExtensionType(4);
+    pub const EXTERNAL_SENDERS: ExtensionType = ExtensionType(5);
+
+    /// Default extension types defined
+    /// in [RFC 9420](https://www.rfc-editor.org/rfc/rfc9420.html#name-leaf-node-contents)
+    pub const DEFAULT: &'static [ExtensionType] = &[
+        ExtensionType::APPLICATION_ID,
+        ExtensionType::RATCHET_TREE,
+        ExtensionType::REQUIRED_CAPABILITIES,
+        ExtensionType::EXTERNAL_PUB,
+        ExtensionType::EXTERNAL_SENDERS,
+    ];
+
+    /// Extension type from a raw value
+    pub const fn new(raw_value: u16) -> Self {
+        ExtensionType(raw_value)
+    }
+
+    /// Raw numerical wrapped value.
+    pub const fn raw_value(&self) -> u16 {
+        self.0
+    }
+
+    /// Determines if this extension type is required to be implemented
+    /// by the MLS RFC.
+    pub const fn is_default(&self) -> bool {
+        self.0 <= 5
+    }
+}
+
+impl From<u16> for ExtensionType {
+    fn from(value: u16) -> Self {
+        ExtensionType(value)
+    }
+}
+
+impl Deref for ExtensionType {
+    type Target = u16;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Debug)]
+#[cfg_attr(feature = "std", derive(thiserror::Error))]
+pub enum ExtensionError {
+    #[cfg_attr(feature = "std", error(transparent))]
+    SerializationError(AnyError),
+    #[cfg_attr(feature = "std", error(transparent))]
+    DeserializationError(AnyError),
+    #[cfg_attr(feature = "std", error("incorrect extension type: {0:?}"))]
+    IncorrectType(ExtensionType),
+}
+
+impl IntoAnyError for ExtensionError {
+    #[cfg(feature = "std")]
+    fn into_dyn_error(self) -> Result<Box<dyn std::error::Error + Send + Sync>, Self> {
+        Ok(self.into())
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+#[non_exhaustive]
+/// An MLS protocol [extension](https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#name-extensions).
+///
+/// Extensions are used as customization points in various parts of the
+/// MLS protocol and are inserted into an [ExtensionList](self::ExtensionList).
+pub struct Extension {
+    /// Extension type of this extension
+    pub extension_type: ExtensionType,
+    /// Data held within this extension
+    #[mls_codec(with = "mls_rs_codec::byte_vec")]
+    pub extension_data: Vec<u8>,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl Extension {
+    /// Create an extension with specified type and data properties.
+    pub fn new(extension_type: ExtensionType, extension_data: Vec<u8>) -> Extension {
+        Extension {
+            extension_type,
+            extension_data,
+        }
+    }
+
+    /// Extension type of this extension
+    #[cfg(feature = "ffi")]
+    pub fn extension_type(&self) -> ExtensionType {
+        self.extension_type
+    }
+
+    /// Data held within this extension
+    #[cfg(feature = "ffi")]
+    pub fn extension_data(&self) -> &[u8] {
+        &self.extension_data
+    }
+}
+
+/// Trait used to convert a type to and from an [Extension]
+pub trait MlsExtension: Sized {
+    /// Error type of the underlying serializer that can convert this type into a `Vec<u8>`.
+    type SerializationError: IntoAnyError;
+
+    /// Error type of the underlying deserializer that can convert a `Vec<u8>` into this type.
+    type DeserializationError: IntoAnyError;
+
+    /// Extension type value that this type represents.
+    fn extension_type() -> ExtensionType;
+
+    /// Convert this type to opaque bytes.
+    fn to_bytes(&self) -> Result<Vec<u8>, Self::SerializationError>;
+
+    /// Create this type from opaque bytes.
+    fn from_bytes(data: &[u8]) -> Result<Self, Self::DeserializationError>;
+
+    /// Convert this type into an [Extension].
+    fn into_extension(self) -> Result<Extension, ExtensionError> {
+        Ok(Extension::new(
+            Self::extension_type(),
+            self.to_bytes()
+                .map_err(|e| ExtensionError::SerializationError(e.into_any_error()))?,
+        ))
+    }
+
+    /// Create this type from an [Extension].
+    fn from_extension(ext: &Extension) -> Result<Self, ExtensionError> {
+        if ext.extension_type != Self::extension_type() {
+            return Err(ExtensionError::IncorrectType(ext.extension_type));
+        }
+
+        Self::from_bytes(&ext.extension_data)
+            .map_err(|e| ExtensionError::DeserializationError(e.into_any_error()))
+    }
+}
+
+/// Convenience trait for custom extension types that use
+/// [mls_rs_codec] as an underlying serialization mechanism
+pub trait MlsCodecExtension: MlsSize + MlsEncode + MlsDecode {
+    fn extension_type() -> ExtensionType;
+}
+
+impl<T> MlsExtension for T
+where
+    T: MlsCodecExtension,
+{
+    type SerializationError = mls_rs_codec::Error;
+    type DeserializationError = mls_rs_codec::Error;
+
+    fn extension_type() -> ExtensionType {
+        <Self as MlsCodecExtension>::extension_type()
+    }
+
+    fn to_bytes(&self) -> Result<Vec<u8>, Self::SerializationError> {
+        self.mls_encode_to_vec()
+    }
+
+    fn from_bytes(data: &[u8]) -> Result<Self, Self::DeserializationError> {
+        Self::mls_decode(&mut &*data)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::convert::Infallible;
+
+    use alloc::vec;
+    use alloc::vec::Vec;
+    use assert_matches::assert_matches;
+    use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+    use super::{Extension, ExtensionError, ExtensionType, MlsCodecExtension, MlsExtension};
+
+    struct TestExtension;
+
+    #[derive(Debug, MlsSize, MlsEncode, MlsDecode)]
+    struct AnotherTestExtension;
+
+    impl MlsExtension for TestExtension {
+        type SerializationError = Infallible;
+        type DeserializationError = Infallible;
+
+        fn extension_type() -> super::ExtensionType {
+            ExtensionType(42)
+        }
+
+        fn to_bytes(&self) -> Result<Vec<u8>, Self::SerializationError> {
+            Ok(vec![0])
+        }
+
+        fn from_bytes(_data: &[u8]) -> Result<Self, Self::DeserializationError> {
+            Ok(TestExtension)
+        }
+    }
+
+    impl MlsCodecExtension for AnotherTestExtension {
+        fn extension_type() -> ExtensionType {
+            ExtensionType(43)
+        }
+    }
+
+    #[test]
+    fn into_extension() {
+        assert_eq!(
+            TestExtension.into_extension().unwrap(),
+            Extension::new(42.into(), vec![0])
+        )
+    }
+
+    #[test]
+    fn incorrect_type_is_discovered() {
+        let ext = Extension::new(42.into(), vec![0]);
+
+        assert_matches!(AnotherTestExtension::from_extension(&ext), Err(ExtensionError::IncorrectType(found)) if found == 42.into());
+    }
+}
diff --git a/src/extension/list.rs b/src/extension/list.rs
new file mode 100644
index 0000000..116c3f9
--- /dev/null
+++ b/src/extension/list.rs
@@ -0,0 +1,363 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use super::{Extension, ExtensionError, ExtensionType, MlsExtension};
+use alloc::vec::Vec;
+use core::ops::Deref;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+/// A collection of MLS [Extensions](super::Extension).
+///
+///
+/// # Warning
+///
+/// Extension lists require that each type of extension has at most one entry.
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+#[derive(Debug, Clone, Default, MlsSize, MlsEncode, Eq)]
+pub struct ExtensionList(Vec<Extension>);
+
+impl Deref for ExtensionList {
+    type Target = Vec<Extension>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl PartialEq for ExtensionList {
+    fn eq(&self, other: &Self) -> bool {
+        self.len() == other.len()
+            && self
+                .iter()
+                .all(|ext| other.get(ext.extension_type).as_ref() == Some(ext))
+    }
+}
+
+impl MlsDecode for ExtensionList {
+    fn mls_decode(reader: &mut &[u8]) -> Result<Self, mls_rs_codec::Error> {
+        mls_rs_codec::iter::mls_decode_collection(reader, |data| {
+            let mut list = ExtensionList::new();
+
+            while !data.is_empty() {
+                let ext = Extension::mls_decode(data)?;
+                let ext_type = ext.extension_type;
+
+                if list.0.iter().any(|e| e.extension_type == ext_type) {
+                    // #[cfg(feature = "std")]
+                    // return Err(mls_rs_codec::Error::Custom(format!(
+                    //    "Extension list has duplicate extension of type {ext_type:?}"
+                    // )));
+
+                    // #[cfg(not(feature = "std"))]
+                    return Err(mls_rs_codec::Error::Custom(1));
+                }
+
+                list.0.push(ext);
+            }
+
+            Ok(list)
+        })
+    }
+}
+
+impl From<Vec<Extension>> for ExtensionList {
+    fn from(extensions: Vec<Extension>) -> Self {
+        extensions.into_iter().collect()
+    }
+}
+
+impl Extend<Extension> for ExtensionList {
+    fn extend<T: IntoIterator<Item = Extension>>(&mut self, iter: T) {
+        iter.into_iter().for_each(|ext| self.set(ext));
+    }
+}
+
+impl FromIterator<Extension> for ExtensionList {
+    fn from_iter<T: IntoIterator<Item = Extension>>(iter: T) -> Self {
+        let mut list = Self::new();
+        list.extend(iter);
+        list
+    }
+}
+
+impl ExtensionList {
+    /// Create a new empty extension list.
+    pub fn new() -> ExtensionList {
+        Default::default()
+    }
+
+    /// Retrieve an extension by providing a type that implements the
+    /// [MlsExtension](super::MlsExtension) trait.
+    ///
+    /// Returns an error if the underlying deserialization of the extension
+    /// data fails.
+    pub fn get_as<E: MlsExtension>(&self) -> Result<Option<E>, ExtensionError> {
+        self.0
+            .iter()
+            .find(|e| e.extension_type == E::extension_type())
+            .map(E::from_extension)
+            .transpose()
+    }
+
+    /// Determine if a specific extension exists within the list.
+    pub fn has_extension(&self, ext_id: ExtensionType) -> bool {
+        self.0.iter().any(|e| e.extension_type == ext_id)
+    }
+
+    /// Set an extension in the list based on a provided type that implements
+    /// the [MlsExtension](super::MlsExtension) trait.
+    ///
+    /// If there is already an entry in the list for the same extension type,
+    /// then the prior value is removed as part of the insertion.
+    ///
+    /// This function will return an error if `ext` fails to serialize
+    /// properly.
+    pub fn set_from<E: MlsExtension>(&mut self, ext: E) -> Result<(), ExtensionError> {
+        let ext = ext.into_extension()?;
+        self.set(ext);
+        Ok(())
+    }
+
+    /// Set an extension in the list based on a raw
+    /// [Extension](super::Extension) value.
+    ///
+    /// If there is already an entry in the list for the same extension type,
+    /// then the prior value is removed as part of the insertion.
+    pub fn set(&mut self, ext: Extension) {
+        let mut found = self
+            .0
+            .iter_mut()
+            .find(|e| e.extension_type == ext.extension_type);
+
+        if let Some(found) = found.take() {
+            *found = ext;
+        } else {
+            self.0.push(ext);
+        }
+    }
+
+    /// Get a raw [Extension](super::Extension) value based on an
+    /// [ExtensionType](super::ExtensionType).
+    pub fn get(&self, extension_type: ExtensionType) -> Option<Extension> {
+        self.0
+            .iter()
+            .find(|e| e.extension_type == extension_type)
+            .cloned()
+    }
+
+    /// Remove an extension from the list by
+    /// [ExtensionType](super::ExtensionType)
+    pub fn remove(&mut self, ext_type: ExtensionType) {
+        self.0.retain(|e| e.extension_type != ext_type)
+    }
+
+    /// Append another extension list to this one.
+    ///
+    /// If there is already an entry in the list for the same extension type,
+    /// then the existing value is removed.
+    pub fn append(&mut self, others: Self) {
+        self.0.extend(others.0);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use alloc::vec;
+    use alloc::vec::Vec;
+    use assert_matches::assert_matches;
+    use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+    use crate::extension::{
+        list::ExtensionList, Extension, ExtensionType, MlsCodecExtension, MlsExtension,
+    };
+
+    #[derive(Debug, Clone, MlsSize, MlsEncode, MlsDecode, PartialEq, Eq)]
+    struct TestExtensionA(u32);
+
+    #[derive(Debug, Clone, MlsEncode, MlsDecode, MlsSize, PartialEq, Eq)]
+    struct TestExtensionB(#[mls_codec(with = "mls_rs_codec::byte_vec")] Vec<u8>);
+
+    #[derive(Debug, Clone, MlsEncode, MlsDecode, MlsSize, PartialEq, Eq)]
+    struct TestExtensionC(u8);
+
+    impl MlsCodecExtension for TestExtensionA {
+        fn extension_type() -> ExtensionType {
+            ExtensionType(128)
+        }
+    }
+
+    impl MlsCodecExtension for TestExtensionB {
+        fn extension_type() -> ExtensionType {
+            ExtensionType(129)
+        }
+    }
+
+    impl MlsCodecExtension for TestExtensionC {
+        fn extension_type() -> ExtensionType {
+            ExtensionType(130)
+        }
+    }
+
+    #[test]
+    fn test_extension_list_get_set_from_get_as() {
+        let mut list = ExtensionList::new();
+
+        let ext_a = TestExtensionA(0);
+        let ext_b = TestExtensionB(vec![1]);
+
+        // Add the extensions to the list
+        list.set_from(ext_a.clone()).unwrap();
+        list.set_from(ext_b.clone()).unwrap();
+
+        assert_eq!(list.len(), 2);
+        assert_eq!(list.get_as::<TestExtensionA>().unwrap(), Some(ext_a));
+        assert_eq!(list.get_as::<TestExtensionB>().unwrap(), Some(ext_b));
+    }
+
+    #[test]
+    fn test_extension_list_get_set() {
+        let mut list = ExtensionList::new();
+
+        let ext_a = Extension::new(ExtensionType(254), vec![0, 1, 2]);
+        let ext_b = Extension::new(ExtensionType(255), vec![4, 5, 6]);
+
+        // Add the extensions to the list
+        list.set(ext_a.clone());
+        list.set(ext_b.clone());
+
+        assert_eq!(list.len(), 2);
+        assert_eq!(list.get(ExtensionType(254)), Some(ext_a));
+        assert_eq!(list.get(ExtensionType(255)), Some(ext_b));
+    }
+
+    #[test]
+    fn extension_list_can_overwrite_values() {
+        let mut list = ExtensionList::new();
+
+        let ext_1 = TestExtensionA(0);
+        let ext_2 = TestExtensionA(1);
+
+        list.set_from(ext_1).unwrap();
+        list.set_from(ext_2.clone()).unwrap();
+
+        assert_eq!(list.get_as::<TestExtensionA>().unwrap(), Some(ext_2));
+    }
+
+    #[test]
+    fn extension_list_will_return_none_for_type_not_stored() {
+        let mut list = ExtensionList::new();
+
+        assert!(list.get_as::<TestExtensionA>().unwrap().is_none());
+
+        assert!(list
+            .get(<TestExtensionA as MlsCodecExtension>::extension_type())
+            .is_none());
+
+        list.set_from(TestExtensionA(1)).unwrap();
+
+        assert!(list.get_as::<TestExtensionB>().unwrap().is_none());
+
+        assert!(list
+            .get(<TestExtensionB as MlsCodecExtension>::extension_type())
+            .is_none());
+    }
+
+    #[test]
+    fn test_extension_list_has_ext() {
+        let mut list = ExtensionList::new();
+
+        let ext = TestExtensionA(255);
+
+        list.set_from(ext).unwrap();
+
+        assert!(list.has_extension(<TestExtensionA as MlsCodecExtension>::extension_type()));
+        assert!(!list.has_extension(42.into()));
+    }
+
+    #[derive(MlsEncode, MlsSize)]
+    struct ExtensionsVec(Vec<Extension>);
+
+    #[test]
+    fn extension_list_is_serialized_like_a_sequence_of_extensions() {
+        let extension_vec = vec![
+            Extension::new(ExtensionType(128), vec![0, 1, 2, 3]),
+            Extension::new(ExtensionType(129), vec![1, 2, 3, 4]),
+        ];
+
+        let extension_list: ExtensionList = ExtensionList::from(extension_vec.clone());
+
+        assert_eq!(
+            ExtensionsVec(extension_vec).mls_encode_to_vec().unwrap(),
+            extension_list.mls_encode_to_vec().unwrap(),
+        );
+    }
+
+    #[test]
+    fn deserializing_extension_list_fails_on_duplicate_extension() {
+        let extensions = ExtensionsVec(vec![
+            TestExtensionA(1).into_extension().unwrap(),
+            TestExtensionA(2).into_extension().unwrap(),
+        ]);
+
+        let serialized_extensions = extensions.mls_encode_to_vec().unwrap();
+
+        assert_matches!(
+            ExtensionList::mls_decode(&mut &*serialized_extensions),
+            Err(mls_rs_codec::Error::Custom(_))
+        );
+    }
+
+    #[test]
+    fn extension_list_equality_does_not_consider_order() {
+        let extensions = [
+            TestExtensionA(33).into_extension().unwrap(),
+            TestExtensionC(34).into_extension().unwrap(),
+        ];
+
+        let a = extensions.iter().cloned().collect::<ExtensionList>();
+        let b = extensions.iter().rev().cloned().collect::<ExtensionList>();
+
+        assert_eq!(a, b);
+    }
+
+    #[test]
+    fn extending_extension_list_maintains_extension_uniqueness() {
+        let mut list = ExtensionList::new();
+        list.set_from(TestExtensionA(33)).unwrap();
+        list.set_from(TestExtensionC(34)).unwrap();
+        list.extend([
+            TestExtensionA(35).into_extension().unwrap(),
+            TestExtensionB(vec![36]).into_extension().unwrap(),
+            TestExtensionA(37).into_extension().unwrap(),
+        ]);
+
+        let expected = ExtensionList(vec![
+            TestExtensionA(37).into_extension().unwrap(),
+            TestExtensionB(vec![36]).into_extension().unwrap(),
+            TestExtensionC(34).into_extension().unwrap(),
+        ]);
+
+        assert_eq!(list, expected);
+    }
+
+    #[test]
+    fn extension_list_from_vec_maintains_extension_uniqueness() {
+        let list = ExtensionList::from(vec![
+            TestExtensionA(33).into_extension().unwrap(),
+            TestExtensionC(34).into_extension().unwrap(),
+            TestExtensionA(35).into_extension().unwrap(),
+        ]);
+
+        let expected = ExtensionList(vec![
+            TestExtensionA(35).into_extension().unwrap(),
+            TestExtensionC(34).into_extension().unwrap(),
+        ]);
+
+        assert_eq!(list, expected);
+    }
+}
diff --git a/src/group.rs b/src/group.rs
new file mode 100644
index 0000000..0acaa76
--- /dev/null
+++ b/src/group.rs
@@ -0,0 +1,11 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+mod group_state;
+mod proposal_type;
+mod roster;
+
+pub use group_state::*;
+pub use proposal_type::*;
+pub use roster::*;
diff --git a/src/group/group_state.rs b/src/group/group_state.rs
new file mode 100644
index 0000000..42ece05
--- /dev/null
+++ b/src/group/group_state.rs
@@ -0,0 +1,85 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use crate::error::IntoAnyError;
+#[cfg(mls_build_async)]
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode};
+
+/// Generic representation of a group's state.
+pub trait GroupState {
+    /// A unique group identifier.
+    fn id(&self) -> Vec<u8>;
+}
+
+/// Generic representation of a prior epoch.
+pub trait EpochRecord {
+    /// A unique epoch identifier within a particular group.
+    fn id(&self) -> u64;
+}
+
+/// Storage that can persist and reload a group state.
+///
+/// A group state is recorded as a combination of the current state
+/// (represented by the [`GroupState`] trait) and some number of prior
+/// group states (represented by the [`EpochRecord`] trait).
+/// This trait implements reading and writing group data as requested by the protocol
+/// implementation.
+///
+/// # Cleaning up records
+///
+/// Group state will not be purged when the local member is removed from the
+/// group. It is up to the implementer of this trait to provide a mechanism
+/// to delete records that can be used by an application.
+///
+
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
+pub trait GroupStateStorage: Send + Sync {
+    type Error: IntoAnyError;
+
+    /// Fetch a group state from storage.
+    async fn state<T>(&self, group_id: &[u8]) -> Result<Option<T>, Self::Error>
+    where
+        T: GroupState + MlsEncode + MlsDecode;
+
+    /// Lazy load cached epoch data from a particular group.
+    async fn epoch<T>(&self, group_id: &[u8], epoch_id: u64) -> Result<Option<T>, Self::Error>
+    where
+        T: EpochRecord + MlsEncode + MlsDecode;
+
+    /// Write pending state updates.
+    ///
+    /// The group id that this update belongs to can be retrieved with
+    /// [`GroupState::id`]. Prior epoch id values can be retrieved with
+    /// [`EpochRecord::id`].
+    ///
+    /// The protocol implementation handles managing the max size of a prior epoch
+    /// cache and the deleting of prior states based on group activity.
+    /// The maximum number of prior epochs that will be stored is controlled by the
+    /// `Preferences::max_epoch_retention` function in `mls_rs`.
+    /// value. Requested deletes are communicated by the `delete_epoch_under`
+    /// parameter being set to `Some`.
+    ///
+    /// # Warning
+    ///
+    /// It is important to consider error recovery when creating an implementation
+    /// of this trait. Calls to [`write`](GroupStateStorage::write) should
+    /// optimally be a single atomic transaction in order to avoid partial writes
+    /// that may corrupt the group state.
+    async fn write<ST, ET>(
+        &mut self,
+        state: ST,
+        epoch_inserts: Vec<ET>,
+        epoch_updates: Vec<ET>,
+    ) -> Result<(), Self::Error>
+    where
+        ST: GroupState + MlsEncode + MlsDecode + Send + Sync,
+        ET: EpochRecord + MlsEncode + MlsDecode + Send + Sync;
+
+    /// The [`EpochRecord::id`] value that is associated with a stored
+    /// prior epoch for a particular group.
+    async fn max_epoch_id(&self, group_id: &[u8]) -> Result<Option<u64>, Self::Error>;
+}
diff --git a/src/group/proposal_type.rs b/src/group/proposal_type.rs
new file mode 100644
index 0000000..b1e6e75
--- /dev/null
+++ b/src/group/proposal_type.rs
@@ -0,0 +1,70 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::fmt::Debug;
+use core::ops::Deref;
+
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+/// Wrapper type representing a proposal type identifier along with default
+/// values defined by the MLS RFC.
+#[derive(
+    Clone, Copy, Eq, Hash, PartialOrd, Ord, PartialEq, MlsSize, MlsEncode, MlsDecode, Debug,
+)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
+#[repr(transparent)]
+pub struct ProposalType(u16);
+
+impl ProposalType {
+    pub const fn new(value: u16) -> ProposalType {
+        ProposalType(value)
+    }
+
+    pub const fn raw_value(&self) -> u16 {
+        self.0
+    }
+}
+
+impl From<ProposalType> for u16 {
+    fn from(value: ProposalType) -> Self {
+        value.0
+    }
+}
+
+impl From<u16> for ProposalType {
+    fn from(value: u16) -> Self {
+        ProposalType(value)
+    }
+}
+
+impl Deref for ProposalType {
+    type Target = u16;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl ProposalType {
+    pub const ADD: ProposalType = ProposalType(1);
+    pub const UPDATE: ProposalType = ProposalType(2);
+    pub const REMOVE: ProposalType = ProposalType(3);
+    pub const PSK: ProposalType = ProposalType(4);
+    pub const RE_INIT: ProposalType = ProposalType(5);
+    pub const EXTERNAL_INIT: ProposalType = ProposalType(6);
+    pub const GROUP_CONTEXT_EXTENSIONS: ProposalType = ProposalType(7);
+
+    /// Default proposal types defined
+    /// in [RFC 9420](https://www.rfc-editor.org/rfc/rfc9420.html#name-leaf-node-contents)
+    pub const DEFAULT: &'static [ProposalType] = &[
+        ProposalType::ADD,
+        ProposalType::UPDATE,
+        ProposalType::REMOVE,
+        ProposalType::PSK,
+        ProposalType::RE_INIT,
+        ProposalType::EXTERNAL_INIT,
+        ProposalType::GROUP_CONTEXT_EXTENSIONS,
+    ];
+}
diff --git a/src/group/roster.rs b/src/group/roster.rs
new file mode 100644
index 0000000..9c93937
--- /dev/null
+++ b/src/group/roster.rs
@@ -0,0 +1,233 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use alloc::vec;
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+use crate::{
+    crypto::CipherSuite,
+    extension::{ExtensionList, ExtensionType},
+    identity::{CredentialType, SigningIdentity},
+    protocol_version::ProtocolVersion,
+};
+
+use super::ProposalType;
+
+#[derive(Clone, PartialEq, Eq, Debug, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+///  Capabilities of a MLS client
+pub struct Capabilities {
+    pub protocol_versions: Vec<ProtocolVersion>,
+    pub cipher_suites: Vec<CipherSuite>,
+    pub extensions: Vec<ExtensionType>,
+    pub proposals: Vec<ProposalType>,
+    pub credentials: Vec<CredentialType>,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl Capabilities {
+    /// Supported protocol versions
+    #[cfg(feature = "ffi")]
+    pub fn protocol_versions(&self) -> &[ProtocolVersion] {
+        &self.protocol_versions
+    }
+
+    /// Supported ciphersuites
+    #[cfg(feature = "ffi")]
+    pub fn cipher_suites(&self) -> &[CipherSuite] {
+        &self.cipher_suites
+    }
+
+    /// Supported extensions
+    #[cfg(feature = "ffi")]
+    pub fn extensions(&self) -> &[ExtensionType] {
+        &self.extensions
+    }
+
+    /// Supported proposals
+    #[cfg(feature = "ffi")]
+    pub fn proposals(&self) -> &[ProposalType] {
+        &self.proposals
+    }
+
+    /// Supported credentials
+    #[cfg(feature = "ffi")]
+    pub fn credentials(&self) -> &[CredentialType] {
+        &self.credentials
+    }
+
+    /// Canonical form
+    pub fn sorted(mut self) -> Self {
+        self.protocol_versions.sort();
+        self.cipher_suites.sort();
+        self.extensions.sort();
+        self.proposals.sort();
+        self.credentials.sort();
+
+        self
+    }
+}
+
+impl Default for Capabilities {
+    fn default() -> Self {
+        use crate::identity::BasicCredential;
+
+        Self {
+            protocol_versions: vec![ProtocolVersion::MLS_10],
+            cipher_suites: CipherSuite::all().collect(),
+            extensions: Default::default(),
+            proposals: Default::default(),
+            credentials: vec![BasicCredential::credential_type()],
+        }
+    }
+}
+
+/// A member of a MLS group.
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct Member {
+    /// The index of this member within a group.
+    ///
+    /// This value is consistent for all clients and will not change as the
+    /// group evolves.
+    pub index: u32,
+    /// Current identity public key and credential of this member.
+    pub signing_identity: SigningIdentity,
+    /// Current client [Capabilities] of this member.
+    pub capabilities: Capabilities,
+    /// Current leaf node extensions in use by this member.
+    pub extensions: ExtensionList,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl Member {
+    pub fn new(
+        index: u32,
+        signing_identity: SigningIdentity,
+        capabilities: Capabilities,
+        extensions: ExtensionList,
+    ) -> Self {
+        Self {
+            index,
+            signing_identity,
+            capabilities,
+            extensions,
+        }
+    }
+
+    /// The index of this member within a group.
+    ///
+    /// This value is consistent for all clients and will not change as the
+    /// group evolves.
+    #[cfg(feature = "ffi")]
+    pub fn index(&self) -> u32 {
+        self.index
+    }
+
+    /// Current identity public key and credential of this member.
+    #[cfg(feature = "ffi")]
+    pub fn signing_identity(&self) -> &SigningIdentity {
+        &self.signing_identity
+    }
+
+    /// Current client [Capabilities] of this member.
+    #[cfg(feature = "ffi")]
+    pub fn capabilities(&self) -> &Capabilities {
+        &self.capabilities
+    }
+
+    /// Current leaf node extensions in use by this member.
+    #[cfg(feature = "ffi")]
+    pub fn extensions(&self) -> &ExtensionList {
+        &self.extensions
+    }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+#[non_exhaustive]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// Update of a member due to a commit.
+pub struct MemberUpdate {
+    pub prior: Member,
+    pub new: Member,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl MemberUpdate {
+    /// Create a new member update.
+    pub fn new(prior: Member, new: Member) -> MemberUpdate {
+        MemberUpdate { prior, new }
+    }
+
+    /// The index that was updated.
+    pub fn index(&self) -> u32 {
+        self.new.index
+    }
+
+    /// Member state before the update.
+    #[cfg(feature = "ffi")]
+    pub fn before_update(&self) -> &Member {
+        &self.prior
+    }
+
+    /// Member state after the update.
+    #[cfg(feature = "ffi")]
+    pub fn after_update(&self) -> &Member {
+        &self.new
+    }
+}
+
+/// A set of roster updates due to a commit.
+#[derive(Clone, Debug, PartialEq)]
+#[non_exhaustive]
+pub struct RosterUpdate {
+    pub(crate) added: Vec<Member>,
+    pub(crate) removed: Vec<Member>,
+    pub(crate) updated: Vec<MemberUpdate>,
+}
+
+impl RosterUpdate {
+    /// Create a new roster update.
+    pub fn new(
+        mut added: Vec<Member>,
+        mut removed: Vec<Member>,
+        mut updated: Vec<MemberUpdate>,
+    ) -> RosterUpdate {
+        added.sort_by_key(|m| m.index);
+        removed.sort_by_key(|m| m.index);
+        updated.sort_by_key(|u| u.index());
+
+        RosterUpdate {
+            added,
+            removed,
+            updated,
+        }
+    }
+    /// Members added via this update.
+    pub fn added(&self) -> &[Member] {
+        &self.added
+    }
+
+    /// Members removed via this update.
+    pub fn removed(&self) -> &[Member] {
+        &self.removed
+    }
+
+    /// Members updated via this update.
+    pub fn updated(&self) -> &[MemberUpdate] {
+        &self.updated
+    }
+}
diff --git a/src/identity.rs b/src/identity.rs
new file mode 100644
index 0000000..2b1b223
--- /dev/null
+++ b/src/identity.rs
@@ -0,0 +1,19 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+mod basic;
+mod credential;
+mod provider;
+mod signing_identity;
+
+#[cfg(feature = "x509")]
+mod x509;
+
+pub use basic::*;
+pub use credential::*;
+pub use provider::*;
+pub use signing_identity::*;
+
+#[cfg(feature = "x509")]
+pub use x509::*;
diff --git a/src/identity/basic.rs b/src/identity/basic.rs
new file mode 100644
index 0000000..a5a52c9
--- /dev/null
+++ b/src/identity/basic.rs
@@ -0,0 +1,68 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::convert::Infallible;
+
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+use super::{Credential, CredentialType, MlsCredential};
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// Bare assertion of an identity without any additional information.
+///
+/// The format of the encoded identity is defined by the application.
+///
+///
+/// # Warning
+///
+/// Basic credentials are inherently insecure since they can not be
+/// properly validated. It is not recommended to use [`BasicCredential`]
+/// in production applications.
+pub struct BasicCredential {
+    /// Underlying identifier as raw bytes.
+    #[mls_codec(with = "mls_rs_codec::byte_vec")]
+    pub identifier: Vec<u8>,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl BasicCredential {
+    /// Create a new basic credential with raw bytes.
+    pub fn new(identifier: Vec<u8>) -> BasicCredential {
+        BasicCredential { identifier }
+    }
+
+    /// Underlying identifier as raw bytes.
+    #[cfg(feature = "ffi")]
+    pub fn identifier(&self) -> &[u8] {
+        &self.identifier
+    }
+}
+
+impl BasicCredential {
+    pub fn credential_type() -> CredentialType {
+        CredentialType::BASIC
+    }
+
+    pub fn into_credential(self) -> Credential {
+        Credential::Basic(self)
+    }
+}
+
+impl MlsCredential for BasicCredential {
+    type Error = Infallible;
+
+    fn credential_type() -> CredentialType {
+        Self::credential_type()
+    }
+
+    fn into_credential(self) -> Result<Credential, Self::Error> {
+        Ok(self.into_credential())
+    }
+}
diff --git a/src/identity/credential.rs b/src/identity/credential.rs
new file mode 100644
index 0000000..c1eaf1c
--- /dev/null
+++ b/src/identity/credential.rs
@@ -0,0 +1,226 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::ops::Deref;
+
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+use super::BasicCredential;
+
+#[cfg(feature = "x509")]
+use super::CertificateChain;
+
+/// Wrapper type representing a credential type identifier along with default
+/// values defined by the MLS RFC.
+#[derive(
+    Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord, MlsSize, MlsEncode, MlsDecode,
+)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
+#[repr(transparent)]
+pub struct CredentialType(u16);
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl CredentialType {
+    /// Basic identity.
+    pub const BASIC: CredentialType = CredentialType(1);
+
+    #[cfg(feature = "x509")]
+    /// X509 Certificate Identity.
+    pub const X509: CredentialType = CredentialType(2);
+
+    pub const fn new(raw_value: u16) -> Self {
+        CredentialType(raw_value)
+    }
+
+    pub const fn raw_value(&self) -> u16 {
+        self.0
+    }
+}
+
+impl From<u16> for CredentialType {
+    fn from(value: u16) -> Self {
+        CredentialType(value)
+    }
+}
+
+impl Deref for CredentialType {
+    type Target = u16;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+#[derive(Clone, Debug, MlsSize, MlsEncode, MlsDecode, PartialEq, Eq, Hash, PartialOrd, Ord)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// Custom user created credential type.
+///
+/// # Warning
+///
+/// In order to use a custom credential within an MLS group, a supporting
+/// [`IdentityProvider`](crate::identity::IdentityProvider) must be created that can
+/// authenticate the credential.
+pub struct CustomCredential {
+    /// Unique credential type to identify this custom credential.
+    pub credential_type: CredentialType,
+    /// Opaque data representing this custom credential.
+    #[mls_codec(with = "mls_rs_codec::byte_vec")]
+    pub data: Vec<u8>,
+}
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl CustomCredential {
+    /// Create a new custom credential with opaque data.
+    ///
+    /// # Warning
+    ///
+    /// Using any of the constants defined within [`CredentialType`] will
+    /// result in unspecified behavior.
+    pub fn new(credential_type: CredentialType, data: Vec<u8>) -> CustomCredential {
+        CustomCredential {
+            credential_type,
+            data,
+        }
+    }
+
+    /// Unique credential type to identify this custom credential.
+    #[cfg(feature = "ffi")]
+    pub fn credential_type(&self) -> CredentialType {
+        self.credential_type
+    }
+
+    /// Opaque data representing this custom credential.
+    #[cfg(feature = "ffi")]
+    pub fn data(&self) -> &[u8] {
+        &self.data
+    }
+}
+
+/// A MLS credential used to authenticate a group member.
+#[derive(Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Hash)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+#[non_exhaustive]
+pub enum Credential {
+    /// Basic identifier-only credential.
+    ///
+    /// # Warning
+    ///
+    /// Basic credentials are inherently insecure since they can not be
+    /// properly validated. It is not recommended to use [`BasicCredential`]
+    /// in production applications.
+    Basic(BasicCredential),
+    #[cfg(feature = "x509")]
+    /// X.509 Certificate chain.
+    X509(CertificateChain),
+    /// User provided custom credential.
+    Custom(CustomCredential),
+}
+
+impl Credential {
+    /// Credential type of the underlying credential.
+    pub fn credential_type(&self) -> CredentialType {
+        match self {
+            Credential::Basic(_) => CredentialType::BASIC,
+            #[cfg(feature = "x509")]
+            Credential::X509(_) => CredentialType::X509,
+            Credential::Custom(c) => c.credential_type,
+        }
+    }
+
+    /// Convert this enum into a [`BasicCredential`]
+    ///
+    /// Returns `None` if this credential is any other type.
+    pub fn as_basic(&self) -> Option<&BasicCredential> {
+        match self {
+            Credential::Basic(basic) => Some(basic),
+            _ => None,
+        }
+    }
+
+    /// Convert this enum into a [`CertificateChain`]
+    ///
+    /// Returns `None` if this credential is any other type.
+    #[cfg(feature = "x509")]
+    pub fn as_x509(&self) -> Option<&CertificateChain> {
+        match self {
+            Credential::X509(chain) => Some(chain),
+            _ => None,
+        }
+    }
+
+    /// Convert this enum into a [`CustomCredential`]
+    ///
+    /// Returns `None` if this credential is any other type.
+    pub fn as_custom(&self) -> Option<&CustomCredential> {
+        match self {
+            Credential::Custom(custom) => Some(custom),
+            _ => None,
+        }
+    }
+}
+
+impl MlsSize for Credential {
+    fn mls_encoded_len(&self) -> usize {
+        let inner_len = match self {
+            Credential::Basic(c) => c.mls_encoded_len(),
+            #[cfg(feature = "x509")]
+            Credential::X509(c) => c.mls_encoded_len(),
+            Credential::Custom(c) => mls_rs_codec::byte_vec::mls_encoded_len(&c.data),
+        };
+
+        self.credential_type().mls_encoded_len() + inner_len
+    }
+}
+
+impl MlsEncode for Credential {
+    fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> {
+        self.credential_type().mls_encode(writer)?;
+
+        match self {
+            Credential::Basic(c) => c.mls_encode(writer),
+            #[cfg(feature = "x509")]
+            Credential::X509(c) => c.mls_encode(writer),
+            Credential::Custom(c) => mls_rs_codec::byte_vec::mls_encode(&c.data, writer),
+        }
+    }
+}
+
+impl MlsDecode for Credential {
+    fn mls_decode(reader: &mut &[u8]) -> Result<Self, mls_rs_codec::Error> {
+        let credential_type = CredentialType::mls_decode(reader)?;
+
+        Ok(match credential_type {
+            CredentialType::BASIC => Credential::Basic(BasicCredential::mls_decode(reader)?),
+            #[cfg(feature = "x509")]
+            CredentialType::X509 => Credential::X509(CertificateChain::mls_decode(reader)?),
+            custom => Credential::Custom(CustomCredential {
+                credential_type: custom,
+                data: mls_rs_codec::byte_vec::mls_decode(reader)?,
+            }),
+        })
+    }
+}
+
+/// Trait that provides a conversion between an underlying credential type and
+/// the [`Credential`] enum.
+pub trait MlsCredential: Sized {
+    /// Conversion error type.
+    type Error;
+
+    /// Credential type represented by this type.
+    fn credential_type() -> CredentialType;
+
+    /// Function to convert this type into a [`Credential`] enum.
+    fn into_credential(self) -> Result<Credential, Self::Error>;
+}
diff --git a/src/identity/provider.rs b/src/identity/provider.rs
new file mode 100644
index 0000000..84c2129
--- /dev/null
+++ b/src/identity/provider.rs
@@ -0,0 +1,70 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use crate::{error::IntoAnyError, extension::ExtensionList, time::MlsTime};
+#[cfg(mls_build_async)]
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+
+use super::{CredentialType, SigningIdentity};
+
+/// Identity system that can be used to validate a
+/// [`SigningIdentity`](mls-rs-core::identity::SigningIdentity)
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
+pub trait IdentityProvider: Send + Sync {
+    /// Error type that this provider returns on internal failure.
+    type Error: IntoAnyError;
+
+    /// Determine if `signing_identity` is valid for a group member.
+    ///
+    /// A `timestamp` value can optionally be supplied to aid with validation
+    /// of a [`Credential`](mls-rs-core::identity::Credential) that requires
+    /// time based context. For example, X.509 certificates can become expired.
+    async fn validate_member(
+        &self,
+        signing_identity: &SigningIdentity,
+        timestamp: Option<MlsTime>,
+        extensions: Option<&ExtensionList>,
+    ) -> Result<(), Self::Error>;
+
+    /// Determine if `signing_identity` is valid for an external sender in
+    /// the ExternalSendersExtension stored in the group context.
+    ///
+    /// A `timestamp` value can optionally be supplied to aid with validation
+    /// of a [`Credential`](mls-rs-core::identity::Credential) that requires
+    /// time based context. For example, X.509 certificates can become expired.
+    async fn validate_external_sender(
+        &self,
+        signing_identity: &SigningIdentity,
+        timestamp: Option<MlsTime>,
+        extensions: Option<&ExtensionList>,
+    ) -> Result<(), Self::Error>;
+
+    /// A unique identifier for `signing_identity`.
+    ///
+    /// The MLS protocol requires that each member of a group has a unique
+    /// set of identifiers according to the application.
+    async fn identity(
+        &self,
+        signing_identity: &SigningIdentity,
+        extensions: &ExtensionList,
+    ) -> Result<Vec<u8>, Self::Error>;
+
+    /// Determines if `successor` can remove `predecessor` as part of an external commit.
+    ///
+    /// The MLS protocol allows for removal of an existing member when adding a
+    /// new member via external commit. This function determines if a removal
+    /// should be allowed by providing the target member to be removed as
+    /// `predecessor` and the new member as `successor`.
+    async fn valid_successor(
+        &self,
+        predecessor: &SigningIdentity,
+        successor: &SigningIdentity,
+        extensions: &ExtensionList,
+    ) -> Result<bool, Self::Error>;
+
+    /// Credential types that are supported by this provider.
+    fn supported_types(&self) -> Vec<CredentialType>;
+}
diff --git a/src/identity/signing_identity.rs b/src/identity/signing_identity.rs
new file mode 100644
index 0000000..870e58f
--- /dev/null
+++ b/src/identity/signing_identity.rs
@@ -0,0 +1,32 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+use crate::crypto::SignaturePublicKey;
+
+use super::Credential;
+
+#[derive(Debug, Clone, Eq, Hash, PartialEq, PartialOrd, Ord, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// MLS group member identity represented as a combination of a
+/// public [`SignaturePublicKey`] and [`Credential`].
+pub struct SigningIdentity {
+    pub signature_key: SignaturePublicKey,
+    pub credential: Credential,
+}
+
+impl SigningIdentity {
+    /// Create a new signing identity from `credential` and `signature_key`
+    pub fn new(credential: Credential, signature_key: SignaturePublicKey) -> SigningIdentity {
+        SigningIdentity {
+            credential,
+            signature_key,
+        }
+    }
+}
diff --git a/src/identity/x509.rs b/src/identity/x509.rs
new file mode 100644
index 0000000..2f7d96c
--- /dev/null
+++ b/src/identity/x509.rs
@@ -0,0 +1,124 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::{
+    convert::Infallible,
+    ops::{Deref, DerefMut},
+};
+
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+use super::{Credential, CredentialType, MlsCredential};
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// X.509 certificate in DER format.
+pub struct DerCertificate(#[mls_codec(with = "mls_rs_codec::byte_vec")] Vec<u8>);
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl DerCertificate {
+    /// Create a der certificate from raw bytes.
+    pub fn new(data: Vec<u8>) -> DerCertificate {
+        DerCertificate(data)
+    }
+
+    /// Convert this certificate into raw bytes.
+    pub fn into_vec(self) -> Vec<u8> {
+        self.0
+    }
+}
+
+impl From<Vec<u8>> for DerCertificate {
+    fn from(data: Vec<u8>) -> Self {
+        DerCertificate(data)
+    }
+}
+
+impl Deref for DerCertificate {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl AsRef<[u8]> for DerCertificate {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// A chain of [`DerCertificate`] that is ordered from leaf to root.
+///
+/// Certificate chains MAY leave out root CA's so long as they are
+/// provided as input to whatever certificate validator ultimately is
+/// verifying the chain.
+pub struct CertificateChain(Vec<DerCertificate>);
+
+impl Deref for CertificateChain {
+    type Target = Vec<DerCertificate>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl DerefMut for CertificateChain {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl From<Vec<DerCertificate>> for CertificateChain {
+    fn from(cert_data: Vec<DerCertificate>) -> Self {
+        CertificateChain(cert_data)
+    }
+}
+
+impl From<Vec<Vec<u8>>> for CertificateChain {
+    fn from(value: Vec<Vec<u8>>) -> Self {
+        CertificateChain(value.into_iter().map(DerCertificate).collect())
+    }
+}
+
+impl FromIterator<DerCertificate> for CertificateChain {
+    fn from_iter<T: IntoIterator<Item = DerCertificate>>(iter: T) -> Self {
+        CertificateChain::from(iter.into_iter().collect::<Vec<_>>())
+    }
+}
+
+impl CertificateChain {
+    /// Get the leaf certificate, which is the first certificate in the chain.
+    pub fn leaf(&self) -> Option<&DerCertificate> {
+        self.0.first()
+    }
+
+    /// Convert this certificate chain into a [`Credential`] enum.
+    pub fn into_credential(self) -> Credential {
+        Credential::X509(self)
+    }
+}
+
+impl MlsCredential for CertificateChain {
+    type Error = Infallible;
+
+    fn credential_type() -> CredentialType {
+        CredentialType::X509
+    }
+
+    fn into_credential(self) -> Result<Credential, Self::Error> {
+        Ok(self.into_credential())
+    }
+}
diff --git a/src/key_package.rs b/src/key_package.rs
new file mode 100644
index 0000000..d2dedaa
--- /dev/null
+++ b/src/key_package.rs
@@ -0,0 +1,67 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+#[cfg(mls_build_async)]
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+use crate::{crypto::HpkeSecretKey, error::IntoAnyError};
+
+#[derive(Debug, Clone, PartialEq, Eq, MlsEncode, MlsDecode, MlsSize)]
+#[non_exhaustive]
+/// Representation of a generated key package and secret keys.
+pub struct KeyPackageData {
+    pub key_package_bytes: Vec<u8>,
+    pub init_key: HpkeSecretKey,
+    pub leaf_node_key: HpkeSecretKey,
+    pub expiration: u64,
+}
+
+impl KeyPackageData {
+    pub fn new(
+        key_package_bytes: Vec<u8>,
+        init_key: HpkeSecretKey,
+        leaf_node_key: HpkeSecretKey,
+        expiration: u64,
+    ) -> KeyPackageData {
+        Self {
+            key_package_bytes,
+            init_key,
+            leaf_node_key,
+            expiration,
+        }
+    }
+}
+
+/// Storage trait that maintains key package secrets.
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
+pub trait KeyPackageStorage: Send + Sync {
+    /// Error type that the underlying storage mechanism returns on internal
+    /// failure.
+    type Error: IntoAnyError;
+
+    /// Delete [`KeyPackageData`] referenced by `id`.
+    ///
+    /// This function is called automatically when the key package referenced
+    /// by `id` is used to successfully join a group.
+    ///
+    /// # Warning
+    ///
+    /// [`KeyPackageData`] internally contains secret key values. The
+    /// provided delete mechanism should securely erase data.
+    async fn delete(&mut self, id: &[u8]) -> Result<(), Self::Error>;
+
+    /// Store [`KeyPackageData`] that can be accessed by `id` in the future.
+    ///
+    /// This function is automatically called whenever a new key package is created.
+    async fn insert(&mut self, id: Vec<u8>, pkg: KeyPackageData) -> Result<(), Self::Error>;
+
+    /// Retrieve [`KeyPackageData`] by its `id`.
+    ///
+    /// `None` should be returned in the event that no key packages are found
+    /// that match `id`.
+    async fn get(&self, id: &[u8]) -> Result<Option<KeyPackageData>, Self::Error>;
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..8dc6088
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,26 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+#![cfg_attr(not(feature = "std"), no_std)]
+#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
+extern crate alloc;
+
+#[cfg(all(test, target_arch = "wasm32"))]
+wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
+
+pub mod crypto;
+pub mod error;
+pub mod extension;
+pub mod group;
+pub mod identity;
+pub mod key_package;
+pub mod protocol_version;
+pub mod psk;
+pub mod secret;
+pub mod time;
+
+pub use mls_rs_codec;
+
+#[cfg(feature = "arbitrary")]
+pub use arbitrary;
diff --git a/src/protocol_version.rs b/src/protocol_version.rs
new file mode 100644
index 0000000..4c9e5ee
--- /dev/null
+++ b/src/protocol_version.rs
@@ -0,0 +1,56 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::ops::Deref;
+
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+
+/// Wrapper type representing a protocol version identifier.
+#[derive(
+    Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, MlsSize, MlsEncode, MlsDecode,
+)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
+#[repr(transparent)]
+pub struct ProtocolVersion(u16);
+
+impl From<u16> for ProtocolVersion {
+    fn from(value: u16) -> Self {
+        ProtocolVersion(value)
+    }
+}
+
+impl From<ProtocolVersion> for u16 {
+    fn from(value: ProtocolVersion) -> Self {
+        value.0
+    }
+}
+
+impl Deref for ProtocolVersion {
+    type Target = u16;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl ProtocolVersion {
+    /// MLS version 1.0
+    pub const MLS_10: ProtocolVersion = ProtocolVersion(1);
+
+    /// Protocol version from a raw value, useful for testing.
+    pub const fn new(value: u16) -> ProtocolVersion {
+        ProtocolVersion(value)
+    }
+
+    /// Raw numerical wrapped value.
+    pub const fn raw_value(&self) -> u16 {
+        self.0
+    }
+
+    /// An iterator over all of the defined MLS versions.
+    pub fn all() -> impl Iterator<Item = ProtocolVersion> {
+        [Self::MLS_10].into_iter()
+    }
+}
diff --git a/src/psk.rs b/src/psk.rs
new file mode 100644
index 0000000..4b26442
--- /dev/null
+++ b/src/psk.rs
@@ -0,0 +1,108 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use crate::error::IntoAnyError;
+#[cfg(mls_build_async)]
+use alloc::boxed::Box;
+use alloc::vec::Vec;
+use core::ops::Deref;
+use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
+use zeroize::Zeroizing;
+
+#[derive(Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
+/// Wrapper type that holds a pre-shared key value and zeroizes on drop.
+pub struct PreSharedKey(#[mls_codec(with = "mls_rs_codec::byte_vec")] Zeroizing<Vec<u8>>);
+
+impl PreSharedKey {
+    /// Create a new PreSharedKey.
+    pub fn new(data: Vec<u8>) -> Self {
+        PreSharedKey(Zeroizing::new(data))
+    }
+
+    /// Raw byte value.
+    pub fn raw_value(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl From<Vec<u8>> for PreSharedKey {
+    fn from(bytes: Vec<u8>) -> Self {
+        Self::new(bytes)
+    }
+}
+
+impl From<Zeroizing<Vec<u8>>> for PreSharedKey {
+    fn from(bytes: Zeroizing<Vec<u8>>) -> Self {
+        Self(bytes)
+    }
+}
+
+impl AsRef<[u8]> for PreSharedKey {
+    fn as_ref(&self) -> &[u8] {
+        self.raw_value()
+    }
+}
+
+impl Deref for PreSharedKey {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        self.raw_value()
+    }
+}
+
+#[derive(Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq, MlsSize, MlsEncode, MlsDecode)]
+#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+/// An external pre-shared key identifier.
+pub struct ExternalPskId(#[mls_codec(with = "mls_rs_codec::byte_vec")] Vec<u8>);
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl ExternalPskId {
+    pub fn new(id_data: Vec<u8>) -> Self {
+        Self(id_data)
+    }
+}
+
+impl AsRef<[u8]> for ExternalPskId {
+    fn as_ref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl Deref for ExternalPskId {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl From<Vec<u8>> for ExternalPskId {
+    fn from(value: Vec<u8>) -> Self {
+        ExternalPskId(value)
+    }
+}
+
+/// Storage trait to maintain a set of pre-shared key values.
+#[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
+#[cfg_attr(mls_build_async, maybe_async::must_be_async)]
+pub trait PreSharedKeyStorage: Send + Sync {
+    /// Error type that the underlying storage mechanism returns on internal
+    /// failure.
+    type Error: IntoAnyError;
+
+    /// Get a pre-shared key by [`ExternalPskId`](ExternalPskId).
+    ///
+    /// `None` should be returned if a pre-shared key can not be found for `id`.
+    async fn get(&self, id: &ExternalPskId) -> Result<Option<PreSharedKey>, Self::Error>;
+
+    /// Determines if a PSK is located within the store
+    async fn contains(&self, id: &ExternalPskId) -> Result<bool, Self::Error> {
+        self.get(id).await.map(|key| key.is_some())
+    }
+}
diff --git a/src/secret.rs b/src/secret.rs
new file mode 100644
index 0000000..524e3bb
--- /dev/null
+++ b/src/secret.rs
@@ -0,0 +1,48 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use alloc::vec::Vec;
+use core::ops::{Deref, DerefMut};
+use zeroize::Zeroizing;
+
+#[cfg_attr(
+    all(feature = "ffi", not(test)),
+    safer_ffi_gen::ffi_type(clone, opaque)
+)]
+#[derive(Clone, Debug, Eq, PartialEq)]
+/// Wrapper struct that represents a zeroize-on-drop `Vec<u8>`
+pub struct Secret(Zeroizing<Vec<u8>>);
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen)]
+impl Secret {
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl From<Vec<u8>> for Secret {
+    fn from(bytes: Vec<u8>) -> Self {
+        Zeroizing::new(bytes).into()
+    }
+}
+
+impl From<Zeroizing<Vec<u8>>> for Secret {
+    fn from(bytes: Zeroizing<Vec<u8>>) -> Self {
+        Self(bytes)
+    }
+}
+
+impl Deref for Secret {
+    type Target = [u8];
+
+    fn deref(&self) -> &[u8] {
+        &self.0
+    }
+}
+
+impl DerefMut for Secret {
+    fn deref_mut(&mut self) -> &mut [u8] {
+        &mut self.0
+    }
+}
diff --git a/src/time.rs b/src/time.rs
new file mode 100644
index 0000000..9c104f7
--- /dev/null
+++ b/src/time.rs
@@ -0,0 +1,66 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// Copyright by contributors to this project.
+// SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+use core::time::Duration;
+
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::prelude::*;
+
+#[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct MlsTime {
+    seconds: u64,
+}
+
+impl MlsTime {
+    /// Create a timestamp from a duration since unix epoch.
+    pub fn from_duration_since_epoch(duration: Duration) -> MlsTime {
+        Self {
+            seconds: duration.as_secs(),
+        }
+    }
+
+    /// Number of seconds since the unix epoch.
+    pub fn seconds_since_epoch(&self) -> u64 {
+        self.seconds
+    }
+}
+
+#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
+impl MlsTime {
+    /// Current system time.
+    pub fn now() -> Self {
+        Self {
+            seconds: std::time::SystemTime::now()
+                .duration_since(std::time::SystemTime::UNIX_EPOCH)
+                .unwrap_or_default()
+                .as_secs(),
+        }
+    }
+}
+
+impl From<u64> for MlsTime {
+    fn from(value: u64) -> Self {
+        Self { seconds: value }
+    }
+}
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen(inline_js = r#"
+export function date_now() {
+  return Date.now();
+}"#)]
+extern "C" {
+    fn date_now() -> f64;
+}
+
+#[cfg(target_arch = "wasm32")]
+impl MlsTime {
+    pub fn now() -> Self {
+        Self {
+            seconds: (date_now() / 1000.0) as u64,
+        }
+    }
+}